summaryrefslogtreecommitdiff
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
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
-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.java (renamed from androidx/app/slice/core/SliceSpecs.java)20
-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.java (renamed from com/android/server/notification/ScheduleCalendar.java)71
-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/widget/CarItemAnimator.java70
-rw-r--r--android/support/car/widget/CarRecyclerView.java142
-rw-r--r--android/support/car/widget/PagedLayoutManager.java1687
-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.java28
-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
-rw-r--r--androidx/app/slice/Slice.java97
-rw-r--r--androidx/app/slice/SliceConvert.java48
-rw-r--r--androidx/app/slice/SliceItem.java22
-rw-r--r--androidx/app/slice/SliceProvider.java21
-rw-r--r--androidx/app/slice/SliceSpec.java87
-rw-r--r--androidx/app/slice/SliceTest.java196
-rw-r--r--androidx/app/slice/SliceTestProvider.java75
-rw-r--r--androidx/app/slice/SliceUtils.java158
-rw-r--r--androidx/app/slice/SliceXml.java244
-rw-r--r--androidx/app/slice/SliceXmlTest.java163
-rw-r--r--androidx/app/slice/builders/GridBuilder.java209
-rw-r--r--androidx/app/slice/builders/ListBuilder.java431
-rw-r--r--androidx/app/slice/builders/MessagingSliceBuilder.java46
-rw-r--r--androidx/app/slice/builders/TemplateSliceBuilder.java78
-rw-r--r--androidx/app/slice/compat/ContentProviderWrapper.java2
-rw-r--r--androidx/app/slice/compat/SliceProviderCompat.java54
-rw-r--r--androidx/app/slice/compat/SliceProviderWrapper.java3
-rw-r--r--androidx/app/slice/core/SliceHints.java20
-rw-r--r--androidx/app/slice/core/SliceQuery.java52
-rw-r--r--androidx/app/slice/widget/ActionRow.java7
-rw-r--r--androidx/app/slice/widget/GridRowView.java354
-rw-r--r--androidx/app/slice/widget/GridView.java203
-rw-r--r--androidx/app/slice/widget/LargeSliceAdapter.java18
-rw-r--r--androidx/app/slice/widget/LargeTemplateView.java33
-rw-r--r--androidx/app/slice/widget/MessageView.java2
-rw-r--r--androidx/app/slice/widget/RowView.java (renamed from androidx/app/slice/widget/SmallTemplateView.java)267
-rw-r--r--androidx/app/slice/widget/ShortcutView.java18
-rw-r--r--androidx/app/slice/widget/SliceLiveData.java6
-rw-r--r--androidx/app/slice/widget/SliceView.java48
-rw-r--r--androidx/car/drawer/CarDrawerActivity.java (renamed from android/support/car/drawer/CarDrawerActivity.java)23
-rw-r--r--androidx/car/drawer/CarDrawerAdapter.java (renamed from android/support/car/drawer/CarDrawerAdapter.java)7
-rw-r--r--androidx/car/drawer/CarDrawerController.java (renamed from android/support/car/drawer/CarDrawerController.java)18
-rw-r--r--androidx/car/drawer/DrawerItemClickListener.java (renamed from android/support/car/drawer/DrawerItemClickListener.java)2
-rw-r--r--androidx/car/drawer/DrawerItemViewHolder.java (renamed from android/support/car/drawer/DrawerItemViewHolder.java)7
-rw-r--r--androidx/car/utils/ColumnCalculator.java (renamed from android/support/car/utils/ColumnCalculator.java)9
-rw-r--r--androidx/car/utils/ListItemBackgroundResolver.java70
-rw-r--r--androidx/car/utils/ListItemBackgroundResolverTest.java73
-rw-r--r--androidx/car/widget/ColumnCardView.java (renamed from android/support/car/widget/ColumnCardView.java)9
-rw-r--r--androidx/car/widget/DayNightStyle.java (renamed from android/support/car/widget/DayNightStyle.java)2
-rw-r--r--androidx/car/widget/ListItem.java701
-rw-r--r--androidx/car/widget/ListItemAdapter.java228
-rw-r--r--androidx/car/widget/ListItemProvider.java56
-rw-r--r--androidx/car/widget/PagedListView.java (renamed from android/support/car/widget/PagedListView.java)535
-rw-r--r--androidx/car/widget/PagedScrollBarView.java (renamed from android/support/car/widget/PagedScrollBarView.java)126
-rw-r--r--androidx/car/widget/PagedSmoothScroller.java74
-rw-r--r--androidx/car/widget/PagedSnapHelper.java269
-rw-r--r--androidx/recyclerview/selection/MouseInputHandler.java4
-rw-r--r--androidx/recyclerview/selection/SelectionHelperBuilder.java5
-rw-r--r--androidx/recyclerview/selection/SelectionStorage.java4
-rw-r--r--androidx/webkit/internal/codegen/BoundaryGeneration.java39
-rw-r--r--androidx/webkit/internal/codegen/BoundaryInterfaceTest.java105
-rw-r--r--androidx/webkit/internal/codegen/Main.java29
-rw-r--r--androidx/webkit/internal/codegen/PsiProjectSetup.java44
-rw-r--r--androidx/webkit/internal/codegen/TestUtils.java101
-rw-r--r--androidx/webkit/internal/codegen/TypeConversionUtils.java64
-rw-r--r--androidx/webkit/internal/codegen/representations/ClassRepr.java73
-rw-r--r--androidx/webkit/internal/codegen/representations/MethodRepr.java62
-rw-r--r--androidx/widget/ViewPager2.java176
-rw-r--r--benchmarks/GetInstancesOfClassesBenchmark.java67
-rw-r--r--com/android/car/setupwizardlib/CarSetupWizardLayout.java273
-rw-r--r--com/android/car/setupwizardlib/util/CarWizardManagerHelper.java123
-rw-r--r--com/android/car/setupwizardlib/util/ResultCodes.java31
-rw-r--r--com/android/commands/content/Content.java64
-rw-r--r--com/android/commands/ime/Ime.java248
-rw-r--r--com/android/commands/input/Input.java4
-rw-r--r--com/android/commands/wm/Wm.java296
-rw-r--r--com/android/defcontainer/DefaultContainerService.java1
-rw-r--r--com/android/ex/photo/ActionBarWrapper.java3
-rw-r--r--com/android/ex/photo/PhotoViewActivity.java8
-rw-r--r--com/android/ims/ImsCall.java15
-rw-r--r--com/android/ims/ImsConfig.java2
-rw-r--r--com/android/ims/ImsManager.java229
-rw-r--r--com/android/ims/ImsReasonInfo.java18
-rw-r--r--com/android/ims/ImsSsData.java189
-rw-r--r--com/android/ims/ImsUt.java71
-rw-r--r--com/android/ims/ImsUtInterface.java27
-rw-r--r--com/android/internal/accessibility/AccessibilityShortcutController.java (renamed from com/android/server/policy/AccessibilityShortcutController.java)168
-rw-r--r--com/android/internal/app/SuggestedLocaleAdapter.java2
-rw-r--r--com/android/internal/app/UnlaunchableAppActivity.java9
-rw-r--r--com/android/internal/app/procstats/ProcessState.java19
-rw-r--r--com/android/internal/app/procstats/ProcessStats.java106
-rw-r--r--com/android/internal/app/procstats/ServiceState.java4
-rw-r--r--com/android/internal/content/FileSystemProvider.java1
-rw-r--r--com/android/internal/graphics/SfVsyncFrameCallbackProvider.java10
-rw-r--r--com/android/internal/inputmethod/InputMethodUtils.java25
-rw-r--r--com/android/internal/location/ProviderRequest.java45
-rw-r--r--com/android/internal/os/BackgroundThread.java2
-rw-r--r--com/android/internal/os/BatteryStatsImpl.java615
-rw-r--r--com/android/internal/os/ByteTransferPipe.java49
-rw-r--r--com/android/internal/os/KernelSingleUidTimeReader.java204
-rw-r--r--com/android/internal/os/KernelUidCpuFreqTimeReader.java19
-rw-r--r--com/android/internal/os/SomeArgs.java1
-rw-r--r--com/android/internal/os/TransferPipe.java17
-rw-r--r--com/android/internal/telephony/BaseCommands.java12
-rw-r--r--com/android/internal/telephony/CarrierActionAgent.java3
-rw-r--r--com/android/internal/telephony/CarrierIdentifier.java60
-rw-r--r--com/android/internal/telephony/CommandException.java3
-rw-r--r--com/android/internal/telephony/CommandsInterface.java20
-rw-r--r--com/android/internal/telephony/GsmCdmaPhone.java25
-rw-r--r--com/android/internal/telephony/IccCard.java12
-rw-r--r--com/android/internal/telephony/IccSmsInterfaceManager.java181
-rw-r--r--com/android/internal/telephony/ImsSMSDispatcher.java18
-rw-r--r--com/android/internal/telephony/MccTable.java15
-rw-r--r--com/android/internal/telephony/NetworkScanRequestTracker.java48
-rw-r--r--com/android/internal/telephony/NitzData.java49
-rw-r--r--com/android/internal/telephony/NitzStateMachine.java652
-rw-r--r--com/android/internal/telephony/Phone.java32
-rw-r--r--com/android/internal/telephony/PhoneInternalInterface.java9
-rw-r--r--com/android/internal/telephony/RIL.java211
-rw-r--r--com/android/internal/telephony/RILConstants.java3
-rw-r--r--com/android/internal/telephony/RadioIndication.java25
-rw-r--r--com/android/internal/telephony/RadioResponse.java123
-rw-r--r--com/android/internal/telephony/SMSDispatcher.java92
-rw-r--r--com/android/internal/telephony/ServiceStateTracker.java598
-rw-r--r--com/android/internal/telephony/SubscriptionInfoUpdater.java88
-rw-r--r--com/android/internal/telephony/TelephonyComponentFactory.java14
-rw-r--r--com/android/internal/telephony/TimeServiceHelper.java179
-rw-r--r--com/android/internal/telephony/UiccSmsController.java31
-rw-r--r--com/android/internal/telephony/cat/CatService.java8
-rw-r--r--com/android/internal/telephony/cat/CommandParamsFactory.java5
-rw-r--r--com/android/internal/telephony/cdma/CdmaSMSDispatcher.java17
-rw-r--r--com/android/internal/telephony/cdma/SmsMessage.java60
-rw-r--r--com/android/internal/telephony/dataconnection/DataCallResponse.java114
-rw-r--r--com/android/internal/telephony/dataconnection/DataConnection.java107
-rw-r--r--com/android/internal/telephony/dataconnection/DcController.java19
-rw-r--r--com/android/internal/telephony/dataconnection/DcTracker.java18
-rw-r--r--com/android/internal/telephony/euicc/EuiccConnector.java31
-rw-r--r--com/android/internal/telephony/euicc/EuiccController.java41
-rw-r--r--com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java4
-rw-r--r--com/android/internal/telephony/gsm/GsmMmiCode.java3
-rw-r--r--com/android/internal/telephony/gsm/GsmSMSDispatcher.java18
-rw-r--r--com/android/internal/telephony/gsm/SmsCbHeader.java11
-rw-r--r--com/android/internal/telephony/gsm/SmsMessage.java113
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhone.java59
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhoneBase.java13
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhoneCall.java4
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java160
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java8
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhoneConnection.java4
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java223
-rw-r--r--com/android/internal/telephony/metrics/TelephonyMetrics.java31
-rw-r--r--com/android/internal/telephony/sip/SipCommandInterface.java8
-rw-r--r--com/android/internal/telephony/sip/SipPhoneBase.java9
-rw-r--r--com/android/internal/telephony/test/SimulatedCommands.java38
-rw-r--r--com/android/internal/telephony/test/SimulatedCommandsVerifier.java18
-rw-r--r--com/android/internal/telephony/uicc/CarrierTestOverride.java13
-rw-r--r--com/android/internal/telephony/uicc/IccCardProxy.java171
-rw-r--r--com/android/internal/telephony/uicc/IccCardStatus.java8
-rw-r--r--com/android/internal/telephony/uicc/IccRecords.java59
-rw-r--r--com/android/internal/telephony/uicc/IccSlotStatus.java88
-rw-r--r--com/android/internal/telephony/uicc/IccUtils.java287
-rw-r--r--com/android/internal/telephony/uicc/IsimUiccRecords.java16
-rw-r--r--com/android/internal/telephony/uicc/RuimRecords.java29
-rw-r--r--com/android/internal/telephony/uicc/SIMRecords.java44
-rw-r--r--com/android/internal/telephony/uicc/UiccCard.java133
-rw-r--r--com/android/internal/telephony/uicc/UiccController.java173
-rw-r--r--com/android/internal/telephony/uicc/UiccSlot.java312
-rw-r--r--com/android/internal/telephony/uicc/UiccStateChangedLauncher.java9
-rw-r--r--com/android/internal/telephony/uicc/asn1/Asn1Decoder.java151
-rw-r--r--com/android/internal/telephony/uicc/asn1/Asn1Node.java598
-rw-r--r--com/android/internal/telephony/uicc/asn1/InvalidAsn1DataException.java45
-rw-r--r--com/android/internal/telephony/uicc/asn1/TagNotFoundException.java38
-rw-r--r--com/android/internal/telephony/uicc/euicc/async/AsyncResultCallback.java46
-rw-r--r--com/android/internal/telephony/uicc/euicc/async/AsyncResultHelper.java71
-rw-r--r--com/android/internal/util/ArrayUtils.java28
-rw-r--r--com/android/internal/util/CollectionUtils.java6
-rw-r--r--com/android/internal/util/FunctionalUtils.java62
-rw-r--r--com/android/internal/util/LatencyTracker.java (renamed from com/android/keyguard/LatencyTracker.java)28
-rw-r--r--com/android/internal/util/Preconditions.java34
-rw-r--r--com/android/internal/util/function/QuadConsumer.java29
-rw-r--r--com/android/internal/util/function/QuadFunction.java29
-rw-r--r--com/android/internal/util/function/QuadPredicate.java29
-rw-r--r--com/android/internal/util/function/TriConsumer.java29
-rw-r--r--com/android/internal/util/function/TriFunction.java (renamed from com/android/server/timezone/ClockHelper.java)14
-rw-r--r--com/android/internal/util/function/TriPredicate.java29
-rw-r--r--com/android/internal/util/function/pooled/ArgumentPlaceholder.java33
-rw-r--r--com/android/internal/util/function/pooled/OmniFunction.java132
-rw-r--r--com/android/internal/util/function/pooled/PooledConsumer.java31
-rw-r--r--com/android/internal/util/function/pooled/PooledFunction.java36
-rw-r--r--com/android/internal/util/function/pooled/PooledLambda.java828
-rw-r--r--com/android/internal/util/function/pooled/PooledLambdaImpl.java557
-rw-r--r--com/android/internal/util/function/pooled/PooledPredicate.java36
-rw-r--r--com/android/internal/util/function/pooled/PooledRunnable.java30
-rw-r--r--com/android/internal/util/function/pooled/PooledSupplier.java59
-rw-r--r--com/android/internal/view/BaseIWindow.java4
-rw-r--r--com/android/internal/view/InputBindResult.java203
-rw-r--r--com/android/internal/view/RotationPolicy.java10
-rw-r--r--com/android/internal/view/menu/ListMenuItemView.java21
-rw-r--r--com/android/internal/widget/ButtonBarLayout.java11
-rw-r--r--com/android/internal/widget/ExploreByTouchHelper.java12
-rw-r--r--com/android/internal/widget/LockPatternUtils.java7
-rw-r--r--com/android/internal/widget/NotificationActionListLayout.java6
-rw-r--r--com/android/internal/widget/RecyclerView.java2
-rw-r--r--com/android/internal/widget/ResolverDrawerLayout.java78
-rw-r--r--com/android/keyguard/KeyguardAbsKeyInputView.java5
-rw-r--r--com/android/keyguard/KeyguardPatternView.java5
-rw-r--r--com/android/keyguard/KeyguardSecurityContainer.java15
-rw-r--r--com/android/keyguard/KeyguardSimPinView.java112
-rw-r--r--com/android/keyguard/KeyguardSimPukView.java116
-rw-r--r--com/android/keyguard/KeyguardSliceView.java265
-rw-r--r--com/android/keyguard/KeyguardStatusView.java53
-rw-r--r--com/android/keyguard/KeyguardUpdateMonitor.java8
-rw-r--r--com/android/keyguard/PasswordTextView.java58
-rw-r--r--com/android/keyguard/ViewMediatorCallback.java6
-rw-r--r--com/android/layoutlib/bridge/Bridge.java655
-rw-r--r--com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java15
-rw-r--r--com/android/layoutlib/bridge/android/BridgeWindow.java5
-rw-r--r--com/android/layoutlib/bridge/android/BridgeWindowSession.java8
-rw-r--r--com/android/providers/settings/SettingsProtoDumpUtil.java6
-rw-r--r--com/android/providers/settings/SettingsProvider.java59
-rw-r--r--com/android/providers/settings/SettingsState.java62
-rw-r--r--com/android/server/AlarmManagerInternal.java21
-rw-r--r--com/android/server/AlarmManagerService.java225
-rw-r--r--com/android/server/AnimationThread.java4
-rw-r--r--com/android/server/BatteryService.java6
-rw-r--r--com/android/server/BluetoothManagerService.java822
-rw-r--r--com/android/server/BootReceiver.java3
-rw-r--r--com/android/server/ConnectivityService.java5
-rw-r--r--com/android/server/DeviceIdleController.java97
-rw-r--r--com/android/server/DisplayThread.java2
-rw-r--r--com/android/server/EntropyMixer.java12
-rw-r--r--com/android/server/FgThread.java2
-rw-r--r--com/android/server/GraphicsStatsService.java18
-rw-r--r--com/android/server/InputMethodManagerService.java541
-rw-r--r--com/android/server/IoThread.java2
-rw-r--r--com/android/server/IpSecService.java704
-rw-r--r--com/android/server/LocationManagerService.java206
-rw-r--r--com/android/server/ServiceWatcher.java1
-rw-r--r--com/android/server/StorageManagerService.java35
-rw-r--r--com/android/server/SystemConfig.java40
-rw-r--r--com/android/server/SystemServer.java34
-rw-r--r--com/android/server/UiThread.java2
-rw-r--r--com/android/server/accessibility/AbstractAccessibilityServiceConnection.java (renamed from com/android/server/accessibility/AccessibilityClientConnection.java)258
-rw-r--r--com/android/server/accessibility/AccessibilityManagerService.java491
-rw-r--r--com/android/server/accessibility/AccessibilityServiceConnection.java13
-rw-r--r--com/android/server/accessibility/AccessibilityShellCommand.java100
-rw-r--r--com/android/server/accessibility/TouchExplorer.java2
-rw-r--r--com/android/server/accessibility/UiAutomationManager.java32
-rw-r--r--com/android/server/accounts/AccountManagerService.java11
-rw-r--r--com/android/server/am/ActiveServices.java29
-rw-r--r--com/android/server/am/ActivityDisplay.java35
-rw-r--r--com/android/server/am/ActivityManagerConstants.java7
-rw-r--r--com/android/server/am/ActivityManagerService.java1396
-rw-r--r--com/android/server/am/ActivityManagerShellCommand.java166
-rw-r--r--com/android/server/am/ActivityRecord.java25
-rw-r--r--com/android/server/am/ActivityStack.java121
-rw-r--r--com/android/server/am/ActivityStackSupervisor.java143
-rw-r--r--com/android/server/am/ActivityStartController.java421
-rw-r--r--com/android/server/am/ActivityStarter.java774
-rw-r--r--com/android/server/am/AppErrors.java12
-rw-r--r--com/android/server/am/AppTaskImpl.java11
-rw-r--r--com/android/server/am/BatteryExternalStatsWorker.java48
-rw-r--r--com/android/server/am/BatteryStatsService.java47
-rw-r--r--com/android/server/am/BroadcastQueue.java15
-rw-r--r--com/android/server/am/ClientLifecycleManager.java11
-rw-r--r--com/android/server/am/LockTaskController.java61
-rw-r--r--com/android/server/am/PendingIntentRecord.java12
-rw-r--r--com/android/server/am/ProcessList.java53
-rw-r--r--com/android/server/am/ProcessRecord.java36
-rw-r--r--com/android/server/am/ProcessStatsService.java18
-rw-r--r--com/android/server/am/RecentTasks.java13
-rw-r--r--com/android/server/am/TaskRecord.java681
-rw-r--r--com/android/server/am/UserController.java26
-rw-r--r--com/android/server/appwidget/AppWidgetServiceImpl.java28
-rw-r--r--com/android/server/audio/AudioService.java21
-rw-r--r--com/android/server/autofill/AutofillManagerService.java66
-rw-r--r--com/android/server/autofill/AutofillManagerServiceImpl.java214
-rw-r--r--com/android/server/autofill/RemoteFillService.java10
-rw-r--r--com/android/server/autofill/Session.java180
-rw-r--r--com/android/server/autofill/ui/FillUi.java120
-rw-r--r--com/android/server/backup/BackupManagerConstants.java22
-rw-r--r--com/android/server/backup/BackupManagerConstantsTest.java109
-rw-r--r--com/android/server/backup/BackupManagerService.java11482
-rw-r--r--com/android/server/backup/BackupManagerServiceInterface.java9
-rw-r--r--com/android/server/backup/FileMetadata.java2
-rw-r--r--com/android/server/backup/PackageManagerBackupAgent.java40
-rw-r--r--com/android/server/backup/RefactoredBackupManagerService.java466
-rw-r--r--com/android/server/backup/Trampoline.java48
-rw-r--r--com/android/server/backup/TransportManager.java338
-rw-r--r--com/android/server/backup/TransportManagerTest.java260
-rw-r--r--com/android/server/backup/internal/BackupHandler.java44
-rw-r--r--com/android/server/backup/internal/PerformBackupTask.java8
-rw-r--r--com/android/server/backup/internal/PerformClearTask.java44
-rw-r--r--com/android/server/backup/internal/PerformInitializeTask.java91
-rw-r--r--com/android/server/backup/internal/PerformInitializeTaskTest.java411
-rw-r--r--com/android/server/backup/internal/RunInitializeReceiver.java32
-rw-r--r--com/android/server/backup/params/ClearParams.java17
-rw-r--r--com/android/server/backup/params/ClearRetryParams.java7
-rw-r--r--com/android/server/backup/params/RestoreGetSetsParams.java20
-rw-r--r--com/android/server/backup/params/RestoreParams.java168
-rw-r--r--com/android/server/backup/restore/ActiveRestoreSession.java217
-rw-r--r--com/android/server/backup/restore/FullRestoreEngine.java2
-rw-r--r--com/android/server/backup/restore/PerformAdbRestoreTask.java2
-rw-r--r--com/android/server/backup/restore/PerformUnifiedRestoreTask.java85
-rw-r--r--com/android/server/backup/restore/RestoreEngine.java4
-rw-r--r--com/android/server/backup/testing/BackupTransportStub.java179
-rw-r--r--com/android/server/backup/testing/ShadowPackageManagerForBackup.java (renamed from com/android/server/backup/testing/DefaultPackageManagerWithQueryIntentServicesAsUser.java)18
-rw-r--r--com/android/server/backup/transport/TransportClient.java16
-rw-r--r--com/android/server/backup/transport/TransportClientManager.java9
-rw-r--r--com/android/server/backup/transport/TransportClientTest.java59
-rw-r--r--com/android/server/backup/transport/TransportNotAvailableException.java4
-rw-r--r--com/android/server/backup/transport/TransportNotRegisteredException.java35
-rw-r--r--com/android/server/backup/transport/TransportUtils.java2
-rw-r--r--com/android/server/backup/utils/BackupManagerMonitorUtils.java2
-rw-r--r--com/android/server/backup/utils/FullBackupUtils.java2
-rw-r--r--com/android/server/backup/utils/TarBackupReader.java4
-rw-r--r--com/android/server/broadcastradio/BroadcastRadioService.java44
-rw-r--r--com/android/server/broadcastradio/hal1/BroadcastRadioService.java66
-rw-r--r--com/android/server/broadcastradio/hal1/Convert.java (renamed from com/android/server/broadcastradio/Convert.java)2
-rw-r--r--com/android/server/broadcastradio/hal1/Tuner.java (renamed from com/android/server/broadcastradio/Tuner.java)2
-rw-r--r--com/android/server/broadcastradio/hal1/TunerCallback.java (renamed from com/android/server/broadcastradio/TunerCallback.java)2
-rw-r--r--com/android/server/broadcastradio/hal2/BroadcastRadioService.java79
-rw-r--r--com/android/server/broadcastradio/hal2/Convert.java132
-rw-r--r--com/android/server/broadcastradio/hal2/RadioModule.java54
-rw-r--r--com/android/server/companion/CompanionDeviceManagerService.java61
-rw-r--r--com/android/server/connectivity/DefaultNetworkMetrics.java14
-rw-r--r--com/android/server/connectivity/NetdEventListenerService.java2
-rw-r--r--com/android/server/content/ContentService.java7
-rw-r--r--com/android/server/content/SyncJobService.java1
-rw-r--r--com/android/server/content/SyncManager.java10
-rw-r--r--com/android/server/content/SyncStorageEngine.java49
-rw-r--r--com/android/server/devicepolicy/BaseIDevicePolicyManager.java99
-rw-r--r--com/android/server/devicepolicy/DevicePolicyManagerService.java1091
-rw-r--r--com/android/server/devicepolicy/NetworkLoggingHandler.java21
-rw-r--r--com/android/server/devicepolicy/OverlayPackagesProvider.java232
-rw-r--r--com/android/server/devicepolicy/Owners.java11
-rw-r--r--com/android/server/devicepolicy/PasswordBlacklist.java165
-rw-r--r--com/android/server/devicepolicy/SecurityLogMonitor.java42
-rw-r--r--com/android/server/display/AutomaticBrightnessController.java31
-rw-r--r--com/android/server/display/BrightnessMappingStrategy.java289
-rw-r--r--com/android/server/display/BrightnessTracker.java38
-rw-r--r--com/android/server/display/ColorDisplayService.java70
-rw-r--r--com/android/server/display/ColorFade.java11
-rw-r--r--com/android/server/display/DisplayManagerService.java119
-rw-r--r--com/android/server/display/DisplayPowerController.java201
-rw-r--r--com/android/server/display/DisplayTransformManager.java3
-rw-r--r--com/android/server/display/PersistentDataStore.java316
-rw-r--r--com/android/server/job/JobSchedulerInternal.java5
-rw-r--r--com/android/server/job/JobSchedulerService.java46
-rw-r--r--com/android/server/job/JobStore.java6
-rw-r--r--com/android/server/location/ContextHubClientBroker.java23
-rw-r--r--com/android/server/location/ContextHubClientManager.java25
-rw-r--r--com/android/server/location/ContextHubService.java487
-rw-r--r--com/android/server/location/ContextHubServiceTransaction.java40
-rw-r--r--com/android/server/location/ContextHubServiceUtil.java44
-rw-r--r--com/android/server/location/ContextHubTransactionManager.java149
-rw-r--r--com/android/server/location/GnssLocationProvider.java397
-rw-r--r--com/android/server/location/GnssMeasurementsProvider.java13
-rw-r--r--com/android/server/location/GnssStatusListenerHelper.java4
-rw-r--r--com/android/server/location/NanoAppStateManager.java192
-rw-r--r--com/android/server/location/RemoteListenerHelper.java15
-rw-r--r--com/android/server/locksettings/LockSettingsService.java145
-rw-r--r--com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java34
-rw-r--r--com/android/server/locksettings/recoverablekeystore/BadPlatformKeyException.java35
-rw-r--r--com/android/server/locksettings/recoverablekeystore/InsecureUserException.java31
-rw-r--r--com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java46
-rw-r--r--com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java60
-rw-r--r--com/android/server/locksettings/recoverablekeystore/KeySyncTask.java350
-rw-r--r--com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java328
-rw-r--r--com/android/server/locksettings/recoverablekeystore/PlatformDecryptionKey.java65
-rw-r--r--com/android/server/locksettings/recoverablekeystore/PlatformEncryptionKey.java62
-rw-r--r--com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java348
-rw-r--r--com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java104
-rw-r--r--com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageException.java35
-rw-r--r--com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java506
-rw-r--r--com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java76
-rw-r--r--com/android/server/locksettings/recoverablekeystore/SecureBox.java471
-rw-r--r--com/android/server/locksettings/recoverablekeystore/WrappedKey.java236
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java668
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java121
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java93
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java184
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java60
-rw-r--r--com/android/server/media/MediaRouterService.java66
-rw-r--r--com/android/server/net/NetworkStatsService.java26
-rw-r--r--com/android/server/notification/ManagedServices.java29
-rw-r--r--com/android/server/notification/NotificationManagerService.java3
-rw-r--r--com/android/server/notification/ScheduleConditionProvider.java112
-rw-r--r--com/android/server/notification/ZenModeHelper.java78
-rw-r--r--com/android/server/oemlock/OemLockService.java14
-rw-r--r--com/android/server/om/OverlayManagerService.java34
-rw-r--r--com/android/server/om/OverlayManagerServiceImpl.java17
-rw-r--r--com/android/server/pm/DumpState.java3
-rw-r--r--com/android/server/pm/Installer.java20
-rw-r--r--com/android/server/pm/InstantAppResolver.java22
-rw-r--r--com/android/server/pm/PackageDexOptimizer.java7
-rw-r--r--com/android/server/pm/PackageInstallerService.java24
-rw-r--r--com/android/server/pm/PackageInstallerSession.java35
-rw-r--r--com/android/server/pm/PackageManagerService.java1689
-rw-r--r--com/android/server/pm/PackageManagerShellCommand.java44
-rw-r--r--com/android/server/pm/PackageSetting.java8
-rw-r--r--com/android/server/pm/PackageSettingBase.java12
-rw-r--r--com/android/server/pm/SELinuxMMAC.java18
-rw-r--r--com/android/server/pm/SettingBase.java1
-rw-r--r--com/android/server/pm/Settings.java38
-rw-r--r--com/android/server/pm/ShortcutNonPersistentUser.java98
-rw-r--r--com/android/server/pm/ShortcutPackage.java8
-rw-r--r--com/android/server/pm/ShortcutPackageInfo.java27
-rw-r--r--com/android/server/pm/ShortcutPackageItem.java4
-rw-r--r--com/android/server/pm/ShortcutService.java72
-rw-r--r--com/android/server/pm/ShortcutUser.java43
-rw-r--r--com/android/server/pm/UserManagerService.java270
-rw-r--r--com/android/server/pm/UserRestrictionsUtils.java13
-rw-r--r--com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java7
-rw-r--r--com/android/server/pm/dex/ArtManagerService.java233
-rw-r--r--com/android/server/pm/permission/BasePermission.java3
-rw-r--r--com/android/server/pm/permission/DefaultPermissionGrantPolicy.java31
-rw-r--r--com/android/server/pm/permission/PermissionManagerService.java24
-rw-r--r--com/android/server/policy/PhoneWindowManager.java216
-rw-r--r--com/android/server/policy/StatusBarController.java80
-rw-r--r--com/android/server/policy/WindowManagerPolicy.java9
-rw-r--r--com/android/server/power/BatterySaverPolicy.java61
-rw-r--r--com/android/server/power/Notifier.java26
-rw-r--r--com/android/server/power/PowerManagerService.java350
-rw-r--r--com/android/server/power/batterysaver/BatterySaverController.java139
-rw-r--r--com/android/server/power/batterysaver/BatterySaverLocationPlugin.java65
-rw-r--r--com/android/server/power/batterysaver/CpuFrequencies.java6
-rw-r--r--com/android/server/print/UserState.java253
-rw-r--r--com/android/server/security/KeyAttestationApplicationIdProviderService.java2
-rw-r--r--com/android/server/slice/PinnedSliceState.java215
-rw-r--r--com/android/server/slice/SliceManagerService.java352
-rw-r--r--com/android/server/soundtrigger/SoundTriggerService.java40
-rw-r--r--com/android/server/stats/StatsCompanionService.java132
-rw-r--r--com/android/server/testing/FrameworkRobolectricTestRunner.java157
-rw-r--r--com/android/server/testing/SystemLoaderClasses.java35
-rw-r--r--com/android/server/timezone/CheckToken.java8
-rw-r--r--com/android/server/timezone/PackageManagerHelper.java2
-rw-r--r--com/android/server/timezone/PackageStatusStorage.java34
-rw-r--r--com/android/server/timezone/PackageTracker.java41
-rw-r--r--com/android/server/timezone/PackageTrackerHelperImpl.java14
-rw-r--r--com/android/server/timezone/PackageVersions.java10
-rw-r--r--com/android/server/timezone/RulesManagerService.java1
-rw-r--r--com/android/server/timezone/RulesManagerServiceHelperImpl.java12
-rw-r--r--com/android/server/trust/TrustAgentWrapper.java2
-rw-r--r--com/android/server/usage/AppIdleHistory.java86
-rw-r--r--com/android/server/usage/AppStandbyController.java190
-rw-r--r--com/android/server/usage/StorageStatsService.java8
-rw-r--r--com/android/server/usage/UsageStatsDatabase.java4
-rw-r--r--com/android/server/usage/UsageStatsService.java108
-rw-r--r--com/android/server/usb/UsbHostManager.java242
-rw-r--r--com/android/server/usb/UsbService.java24
-rw-r--r--com/android/server/usb/descriptors/UsbConfigDescriptor.java8
-rw-r--r--com/android/server/usb/descriptors/UsbDescriptorParser.java37
-rw-r--r--com/android/server/usb/descriptors/UsbDeviceDescriptor.java13
-rw-r--r--com/android/server/usb/descriptors/UsbEndpointDescriptor.java8
-rw-r--r--com/android/server/usb/descriptors/UsbInterfaceDescriptor.java7
-rw-r--r--com/android/server/voiceinteraction/VoiceInteractionManagerService.java28
-rw-r--r--com/android/server/vr/VrManagerInternal.java14
-rw-r--r--com/android/server/vr/VrManagerService.java48
-rw-r--r--com/android/server/wallpaper/IWallpaperManagerService.java35
-rw-r--r--com/android/server/wallpaper/WallpaperManagerService.java71
-rw-r--r--com/android/server/webkit/SystemImpl.java5
-rw-r--r--com/android/server/webkit/SystemInterface.java2
-rw-r--r--com/android/server/webkit/WebViewUpdater.java18
-rw-r--r--com/android/server/wifi/ExtendedWifiInfo.java123
-rw-r--r--com/android/server/wifi/FrameworkFacade.java18
-rw-r--r--com/android/server/wifi/HalDeviceManager.java274
-rw-r--r--com/android/server/wifi/LegacyConnectedScore.java44
-rw-r--r--com/android/server/wifi/SavedNetworkEvaluator.java123
-rw-r--r--com/android/server/wifi/SoftApManager.java269
-rw-r--r--com/android/server/wifi/StateMachineDeathRecipient.java86
-rw-r--r--com/android/server/wifi/SupplicantStaIfaceHal.java855
-rw-r--r--com/android/server/wifi/VelocityBasedConnectedScore.java26
-rw-r--r--com/android/server/wifi/WakeupConfigStoreData.java179
-rw-r--r--com/android/server/wifi/WakeupController.java162
-rw-r--r--com/android/server/wifi/WakeupLock.java142
-rw-r--r--com/android/server/wifi/WifiCertManager.java156
-rw-r--r--com/android/server/wifi/WifiConfigManager.java3
-rw-r--r--com/android/server/wifi/WifiConnectivityManager.java42
-rw-r--r--com/android/server/wifi/WifiController.java34
-rw-r--r--com/android/server/wifi/WifiCountryCode.java18
-rw-r--r--com/android/server/wifi/WifiDiagnostics.java4
-rw-r--r--com/android/server/wifi/WifiInjector.java50
-rw-r--r--com/android/server/wifi/WifiLinkLayerStats.java132
-rw-r--r--com/android/server/wifi/WifiNative.java840
-rw-r--r--com/android/server/wifi/WifiNetworkSelector.java5
-rw-r--r--com/android/server/wifi/WifiScoreReport.java2
-rw-r--r--com/android/server/wifi/WifiServiceImpl.java88
-rw-r--r--com/android/server/wifi/WifiStateMachine.java148
-rw-r--r--com/android/server/wifi/WifiStateMachinePrime.java88
-rw-r--r--com/android/server/wifi/WifiVendorHal.java497
-rw-r--r--com/android/server/wifi/WificondControl.java360
-rw-r--r--com/android/server/wifi/aware/WifiAwareClientState.java2
-rw-r--r--com/android/server/wifi/aware/WifiAwareDataPathStateManager.java34
-rw-r--r--com/android/server/wifi/aware/WifiAwareDiscoverySessionState.java6
-rw-r--r--com/android/server/wifi/aware/WifiAwareMetrics.java3
-rw-r--r--com/android/server/wifi/aware/WifiAwareNativeApi.java28
-rw-r--r--com/android/server/wifi/aware/WifiAwareNativeCallback.java52
-rw-r--r--com/android/server/wifi/aware/WifiAwareNativeManager.java105
-rw-r--r--com/android/server/wifi/aware/WifiAwareService.java4
-rw-r--r--com/android/server/wifi/aware/WifiAwareServiceImpl.java92
-rw-r--r--com/android/server/wifi/aware/WifiAwareStateManager.java82
-rw-r--r--com/android/server/wifi/hotspot2/OsuNetworkConnection.java136
-rw-r--r--com/android/server/wifi/hotspot2/OsuServerConnection.java191
-rw-r--r--com/android/server/wifi/hotspot2/PasspointConfigStoreData.java3
-rw-r--r--com/android/server/wifi/hotspot2/PasspointManager.java5
-rw-r--r--com/android/server/wifi/hotspot2/PasspointObjectFactory.java15
-rw-r--r--com/android/server/wifi/hotspot2/PasspointProvider.java7
-rw-r--r--com/android/server/wifi/hotspot2/PasspointProvisioner.java274
-rw-r--r--com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java183
-rw-r--r--com/android/server/wifi/rtt/RttNative.java175
-rw-r--r--com/android/server/wifi/rtt/RttService.java8
-rw-r--r--com/android/server/wifi/rtt/RttServiceImpl.java252
-rw-r--r--com/android/server/wifi/scanner/WifiScanningServiceImpl.java20
-rw-r--r--com/android/server/wifi/scanner/WificondScannerImpl.java489
-rw-r--r--com/android/server/wifi/util/WifiPermissionsUtil.java26
-rw-r--r--com/android/server/wifi/util/WifiPermissionsWrapper.java12
-rw-r--r--com/android/server/wm/AccessibilityController.java19
-rw-r--r--com/android/server/wm/AnimationAdapter.java80
-rw-r--r--com/android/server/wm/AppTransition.java66
-rw-r--r--com/android/server/wm/AppWindowAnimator.java495
-rw-r--r--com/android/server/wm/AppWindowContainerController.java30
-rw-r--r--com/android/server/wm/AppWindowThumbnail.java164
-rw-r--r--com/android/server/wm/AppWindowToken.java485
-rw-r--r--com/android/server/wm/DisplayContent.java261
-rw-r--r--com/android/server/wm/DisplayFrames.java73
-rw-r--r--com/android/server/wm/DockedStackDividerController.java42
-rw-r--r--com/android/server/wm/DragDropController.java326
-rw-r--r--com/android/server/wm/DragState.java8
-rw-r--r--com/android/server/wm/InputMonitor.java5
-rw-r--r--com/android/server/wm/LocalAnimationAdapter.java119
-rw-r--r--com/android/server/wm/RemoteEventTrace.java7
-rw-r--r--com/android/server/wm/RemoteSurfaceTrace.java78
-rw-r--r--com/android/server/wm/RootWindowContainer.java10
-rw-r--r--com/android/server/wm/Session.java21
-rw-r--r--com/android/server/wm/SurfaceAnimationRunner.java243
-rw-r--r--com/android/server/wm/SurfaceAnimationThread.java59
-rw-r--r--com/android/server/wm/SurfaceAnimator.java377
-rw-r--r--com/android/server/wm/Task.java25
-rw-r--r--com/android/server/wm/TaskPositioner.java7
-rw-r--r--com/android/server/wm/TaskPositioningController.java168
-rw-r--r--com/android/server/wm/TaskSnapshotController.java29
-rw-r--r--com/android/server/wm/TaskSnapshotSurface.java14
-rw-r--r--com/android/server/wm/TaskStack.java100
-rw-r--r--com/android/server/wm/TaskTapPointerEventListener.java4
-rw-r--r--com/android/server/wm/TaskWindowContainerController.java10
-rw-r--r--com/android/server/wm/TransactionFactory.java27
-rw-r--r--com/android/server/wm/WallpaperController.java39
-rw-r--r--com/android/server/wm/WallpaperWindowToken.java18
-rw-r--r--com/android/server/wm/WindowAnimationSpec.java166
-rw-r--r--com/android/server/wm/WindowAnimator.java25
-rw-r--r--com/android/server/wm/WindowContainer.java371
-rw-r--r--com/android/server/wm/WindowManagerInternal.java36
-rw-r--r--com/android/server/wm/WindowManagerService.java345
-rw-r--r--com/android/server/wm/WindowManagerShellCommand.java286
-rw-r--r--com/android/server/wm/WindowManagerThreadPriorityBooster.java11
-rw-r--r--com/android/server/wm/WindowState.java380
-rw-r--r--com/android/server/wm/WindowStateAnimator.java608
-rw-r--r--com/android/server/wm/WindowSurfaceController.java8
-rw-r--r--com/android/server/wm/WindowSurfacePlacer.java129
-rw-r--r--com/android/server/wm/WindowToken.java42
-rw-r--r--com/android/settingslib/HelpUtils.java2
-rw-r--r--com/android/settingslib/RestrictedLockUtils.java50
-rw-r--r--com/android/settingslib/Utils.java16
-rw-r--r--com/android/settingslib/applications/ApplicationsState.java13
-rw-r--r--com/android/settingslib/applications/ServiceListing.java226
-rw-r--r--com/android/settingslib/bluetooth/CachedBluetoothDevice.java3
-rw-r--r--com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java2
-rw-r--r--com/android/settingslib/bluetooth/LocalBluetoothManager.java1
-rw-r--r--com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java16
-rw-r--r--com/android/settingslib/bluetooth/PbapServerProfile.java2
-rw-r--r--com/android/settingslib/datetime/ZoneGetter.java2
-rw-r--r--com/android/settingslib/drawer/DashboardCategory.java20
-rw-r--r--com/android/settingslib/fuelgauge/PowerWhitelistBackend.java108
-rw-r--r--com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java292
-rw-r--r--com/android/settingslib/license/LicenseHtmlLoader.java110
-rw-r--r--com/android/settingslib/location/RecentLocationApps.java47
-rw-r--r--com/android/settingslib/utils/AsyncLoader.java110
-rw-r--r--com/android/settingslib/wifi/AccessPoint.java168
-rw-r--r--com/android/settingslib/wifi/WifiUtils.java197
-rw-r--r--com/android/setupwizardlib/GlifLayoutTest.java2
-rw-r--r--com/android/setupwizardlib/items/ButtonItemDrawingTest.java57
-rw-r--r--com/android/setupwizardlib/items/ButtonItemTest.java5
-rw-r--r--com/android/setupwizardlib/items/ExpandableSwitchItemTest.java3
-rw-r--r--com/android/setupwizardlib/items/ItemGroupTest.java5
-rw-r--r--com/android/setupwizardlib/items/SwitchItemTest.java3
-rw-r--r--com/android/setupwizardlib/robolectric/PatchedGradleManifestFactory.java126
-rw-r--r--com/android/setupwizardlib/robolectric/SuwLibRobolectricTestRunner.java29
-rw-r--r--com/android/setupwizardlib/span/LinkSpanTest.java4
-rw-r--r--com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java3
-rw-r--r--com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java3
-rw-r--r--com/android/setupwizardlib/template/RequireScrollMixinTest.java3
-rw-r--r--com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java3
-rw-r--r--com/android/setupwizardlib/util/DimensionConsistencyTest.java3
-rw-r--r--com/android/setupwizardlib/util/GlifDimensionTest.java3
-rw-r--r--com/android/setupwizardlib/util/GlifStyleTest.java14
-rw-r--r--com/android/setupwizardlib/util/LinkAccessibilityHelper.java5
-rw-r--r--com/android/setupwizardlib/util/PartnerTest.java2
-rw-r--r--com/android/setupwizardlib/util/WizardManagerHelper.java28
-rw-r--r--com/android/setupwizardlib/util/WizardManagerHelperTest.java143
-rw-r--r--com/android/setupwizardlib/view/FillContentLayoutTest.java3
-rw-r--r--com/android/setupwizardlib/view/IllustrationVideoViewTest.java2
-rw-r--r--com/android/setupwizardlib/view/NavigationBarButton.java2
-rw-r--r--com/android/shell/Screenshooter.java82
-rw-r--r--com/android/systemui/EmulatedDisplayCutout.java136
-rw-r--r--com/android/systemui/LatencyTester.java2
-rw-r--r--com/android/systemui/SystemUIApplication.java68
-rw-r--r--com/android/systemui/SystemUIFactory.java44
-rw-r--r--com/android/systemui/car/CarNotificationEntryManager.java54
-rw-r--r--com/android/systemui/car/CarSystemUIFactory.java15
-rw-r--r--com/android/systemui/doze/AlwaysOnDisplayPolicy.java46
-rw-r--r--com/android/systemui/doze/DozeFactory.java8
-rw-r--r--com/android/systemui/doze/DozeHost.java1
-rw-r--r--com/android/systemui/doze/DozeService.java3
-rw-r--r--com/android/systemui/doze/DozeUi.java9
-rw-r--r--com/android/systemui/doze/DozeWallpaperState.java84
-rw-r--r--com/android/systemui/globalactions/GlobalActionsDialog.java49
-rw-r--r--com/android/systemui/keyguard/KeyguardSliceProvider.java63
-rw-r--r--com/android/systemui/keyguard/KeyguardViewMediator.java47
-rw-r--r--com/android/systemui/media/NotificationPlayer.java23
-rw-r--r--com/android/systemui/pip/phone/PipManager.java21
-rw-r--r--com/android/systemui/pip/phone/PipMenuActivity.java39
-rw-r--r--com/android/systemui/pip/phone/PipNotificationController.java231
-rw-r--r--com/android/systemui/pip/phone/PipTouchHandler.java8
-rw-r--r--com/android/systemui/qs/QSFooterImpl.java117
-rw-r--r--com/android/systemui/qs/QSFragment.java6
-rw-r--r--com/android/systemui/recents/RecentsActivity.java3
-rw-r--r--com/android/systemui/recents/RecentsImpl.java49
-rw-r--r--com/android/systemui/recents/events/component/ExpandPipEvent.java1
-rw-r--r--com/android/systemui/recents/misc/SystemServicesProxy.java2
-rw-r--r--com/android/systemui/recents/views/TaskStackViewTouchHandler.java3
-rw-r--r--com/android/systemui/screenshot/GlobalScreenshot.java88
-rw-r--r--com/android/systemui/shared/system/ActivityManagerWrapper.java11
-rw-r--r--com/android/systemui/shared/system/TaskStackChangeListener.java3
-rw-r--r--com/android/systemui/shared/system/TaskStackChangeListeners.java40
-rw-r--r--com/android/systemui/statusbar/ExpandableNotificationRow.java52
-rw-r--r--com/android/systemui/statusbar/ExpandableOutlineView.java19
-rw-r--r--com/android/systemui/statusbar/ExpandableView.java3
-rw-r--r--com/android/systemui/statusbar/NotificationEntryManager.java960
-rw-r--r--com/android/systemui/statusbar/NotificationGutsManager.java57
-rw-r--r--com/android/systemui/statusbar/NotificationListContainer.java182
-rw-r--r--com/android/systemui/statusbar/NotificationListener.java145
-rw-r--r--com/android/systemui/statusbar/NotificationLockscreenUserManager.java457
-rw-r--r--com/android/systemui/statusbar/NotificationLogger.java227
-rw-r--r--com/android/systemui/statusbar/NotificationMediaManager.java34
-rw-r--r--com/android/systemui/statusbar/NotificationPresenter.java85
-rw-r--r--com/android/systemui/statusbar/NotificationRemoteInputManager.java446
-rw-r--r--com/android/systemui/statusbar/NotificationShelf.java33
-rw-r--r--com/android/systemui/statusbar/NotificationSnooze.java18
-rw-r--r--com/android/systemui/statusbar/NotificationUpdateHandler.java58
-rw-r--r--com/android/systemui/statusbar/NotificationViewHierarchyManager.java345
-rw-r--r--com/android/systemui/statusbar/RemoteInputController.java54
-rw-r--r--com/android/systemui/statusbar/car/CarStatusBar.java29
-rw-r--r--com/android/systemui/statusbar/notification/NotificationUtils.java5
-rw-r--r--com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java4
-rw-r--r--com/android/systemui/statusbar/phone/DozeParameters.java50
-rw-r--r--com/android/systemui/statusbar/phone/FingerprintUnlockController.java3
-rw-r--r--com/android/systemui/statusbar/phone/KeyguardBouncer.java6
-rw-r--r--com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java12
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarFragment.java10
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarView.java10
-rw-r--r--com/android/systemui/statusbar/phone/NotificationIconContainer.java122
-rw-r--r--com/android/systemui/statusbar/phone/NotificationPanelView.java5
-rw-r--r--com/android/systemui/statusbar/phone/PanelView.java25
-rw-r--r--com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java1
-rw-r--r--com/android/systemui/statusbar/phone/ScrimController.java118
-rw-r--r--com/android/systemui/statusbar/phone/ScrimState.java48
-rw-r--r--com/android/systemui/statusbar/phone/StatusBar.java2642
-rw-r--r--com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java4
-rw-r--r--com/android/systemui/statusbar/phone/StatusBarWindowManager.java30
-rw-r--r--com/android/systemui/statusbar/phone/StatusBarWindowView.java1
-rw-r--r--com/android/systemui/statusbar/policy/LocationControllerImpl.java7
-rw-r--r--com/android/systemui/statusbar/policy/MobileSignalController.java6
-rw-r--r--com/android/systemui/statusbar/policy/NetworkControllerImpl.java20
-rw-r--r--com/android/systemui/statusbar/policy/RotationLockController.java1
-rw-r--r--com/android/systemui/statusbar/policy/RotationLockControllerImpl.java4
-rw-r--r--com/android/systemui/statusbar/stack/ExpandableViewState.java15
-rw-r--r--com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java78
-rw-r--r--com/android/systemui/statusbar/stack/StackScrollAlgorithm.java34
-rw-r--r--com/android/systemui/statusbar/stack/StackScrollState.java1
-rw-r--r--com/android/systemui/statusbar/stack/StackStateAnimator.java2
-rw-r--r--com/android/systemui/volume/Events.java4
-rw-r--r--com/android/systemui/volume/OutputChooserDialog.java401
-rw-r--r--com/android/systemui/volume/OutputChooserLayout.java258
-rw-r--r--com/android/systemui/volume/VolumeDialogComponent.java4
-rw-r--r--com/android/systemui/volume/VolumeDialogControllerImpl.java17
-rw-r--r--com/android/systemui/volume/VolumeDialogImpl.java386
-rw-r--r--com/android/systemui/volume/VolumeUI.java6
-rw-r--r--com/android/systemui/volume/VolumeUiLayout.java66
-rw-r--r--com/android/systemui/volume/car/CarVolumeDialogController.java10
-rw-r--r--com/example/android/nn/benchmark/NNBenchmark.java267
-rw-r--r--com/example/android/nn/benchmark/NNControls.java225
-rw-r--r--com/example/android/nn/benchmark/NNSettings.java65
-rw-r--r--com/example/android/nn/benchmark/NNTest.java125
-rw-r--r--com/example/android/nn/benchmark/NNTestBase.java111
-rw-r--r--com/example/android/nn/benchmark/NNTestList.java57
-rw-r--r--foo/bar/ComplexDao.java464
-rw-r--r--foo/bar/ComplexDatabase.java118
-rw-r--r--foo/bar/DeletionDao.java271
-rw-r--r--foo/bar/UpdateDao.java280
-rw-r--r--foo/bar/WriterDao.java136
-rw-r--r--java/io/CharArrayReader.java6
-rw-r--r--java/io/FileInputStream.java83
-rw-r--r--java/io/FileOutputStream.java93
-rw-r--r--java/io/FileSystem.java1
-rw-r--r--java/io/StringBufferInputStream.java3
-rw-r--r--java/lang/Iterable.java1
-rw-r--r--java/lang/reflect/AccessibleObject.java3
-rw-r--r--java/lang/reflect/Constructor.java28
-rw-r--r--java/lang/reflect/Executable.java34
-rw-r--r--java/lang/reflect/Field.java61
-rw-r--r--java/lang/reflect/MalformedParametersException.java1
-rw-r--r--java/lang/reflect/Method.java39
-rw-r--r--java/lang/reflect/Modifier.java18
-rw-r--r--java/lang/reflect/Parameter.java11
-rw-r--r--java/lang/reflect/Proxy.java200
-rw-r--r--java/lang/reflect/Type.java3
-rw-r--r--java/lang/reflect/TypeVariable.java8
-rw-r--r--java/net/InMemoryCookieStore.java4
-rw-r--r--java/nio/Bits.java316
-rw-r--r--java/nio/Buffer.java161
-rw-r--r--java/nio/BufferOverflowException.java6
-rw-r--r--java/nio/BufferUnderflowException.java6
-rw-r--r--java/nio/ByteBuffer.java869
-rw-r--r--java/nio/ByteOrder.java13
-rw-r--r--java/nio/CharBuffer.java602
-rw-r--r--java/nio/DoubleBuffer.java426
-rw-r--r--java/nio/FloatBuffer.java424
-rw-r--r--java/nio/IntBuffer.java396
-rw-r--r--java/nio/InvalidMarkException.java6
-rw-r--r--java/nio/LongBuffer.java405
-rw-r--r--java/nio/MappedByteBuffer.java23
-rw-r--r--java/nio/ShortBuffer.java404
-rw-r--r--java/nio/StringCharBuffer.java25
-rw-r--r--java/nio/channels/ServerSocketChannel.java2
-rw-r--r--java/nio/file/FileSystem.java1
-rw-r--r--java/text/SimpleDateFormat.java4
-rw-r--r--java/util/TimeZone.java2
-rw-r--r--java/util/concurrent/ConcurrentHashMap.java2
-rw-r--r--java/util/concurrent/TimeUnit.java53
-rw-r--r--org/chromium/support_lib_boundary/FilterMethodsBoundaryInterface.java5
-rw-r--r--org/chromium/support_lib_boundary/SingleClassAndMethodBoundaryInterface.java5
-rw-r--r--org/chromium/support_lib_boundary/WebKitTypeAsMethodParameterBoundaryInterface.java11
-rw-r--r--org/chromium/support_lib_boundary/WebKitTypeAsMethodReturnBoundaryInterface.java9
1378 files changed, 97059 insertions, 50505 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/androidx/app/slice/core/SliceSpecs.java b/android/app/servertransaction/ObjectPoolItem.java
index a21633ba..17bd4f30 100644
--- a/androidx/app/slice/core/SliceSpecs.java
+++ b/android/app/servertransaction/ObjectPoolItem.java
@@ -14,20 +14,16 @@
* limitations under the License.
*/
-package androidx.app.slice.core;
-
-import android.app.slice.SliceSpec;
-import android.support.annotation.RestrictTo;
-
-import java.util.Collections;
-import java.util.List;
+package android.app.servertransaction;
/**
+ * Base interface for all lifecycle items that can be put in object pool.
* @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class SliceSpecs {
-
- // TODO: Fill these in.
- public static List<SliceSpec> SUPPORTED_SPECS = Collections.emptyList();
+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/com/android/server/notification/ScheduleCalendar.java b/android/service/notification/ScheduleCalendar.java
index 40230bd2..8a7ff4da 100644
--- a/com/android/server/notification/ScheduleCalendar.java
+++ b/android/service/notification/ScheduleCalendar.java
@@ -1,11 +1,11 @@
-/**
- * Copyright (c) 2014, The Android Open Source Project
+/*
+ * 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
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,16 +14,22 @@
* limitations under the License.
*/
-package com.android.server.notification;
+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();
@@ -34,25 +40,63 @@ public class ScheduleCalendar {
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) {
- if (mSchedule.exitAtAlarm
- && (now > mSchedule.nextAlarm || nextAlarm < mSchedule.nextAlarm)) {
- mSchedule.nextAlarm = 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);
@@ -76,6 +120,10 @@ public class ScheduleCalendar {
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);
@@ -86,7 +134,14 @@ public class ScheduleCalendar {
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;
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/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/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/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/media/tv/Utils.java b/android/support/media/tv/Utils.java
new file mode 100644
index 00000000..a6ff0ad9
--- /dev/null
+++ b/android/support/media/tv/Utils.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.support.media.tv;
+
+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
diff --git a/androidx/app/slice/Slice.java b/androidx/app/slice/Slice.java
index fb2395e1..3c5f1bf6 100644
--- a/androidx/app/slice/Slice.java
+++ b/androidx/app/slice/Slice.java
@@ -26,13 +26,15 @@ import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_SELECTED;
import static android.app.slice.Slice.HINT_TITLE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
+import static androidx.app.slice.SliceConvert.unwrap;
+
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.app.RemoteInput;
@@ -52,23 +54,27 @@ import android.support.v4.os.BuildCompat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import androidx.app.slice.compat.SliceProviderCompat;
import androidx.app.slice.core.SliceHints;
-import androidx.app.slice.core.SliceSpecs;
/**
* A slice is a piece of app content and actions that can be surfaced outside of the app.
*
- * <p>They are constructed using {@link Builder} in a tree structure
- * that provides the OS some information about how the content should be displayed.
+ * <p>They are constructed using {@link androidx.app.slice.builders.TemplateSliceBuilder}s
+ * in a tree structure that provides the OS some information about how the content should be
+ * displayed.
*/
public final class Slice {
private static final String HINTS = "hints";
private static final String ITEMS = "items";
private static final String URI = "uri";
+ private static final String SPEC_TYPE = "type";
+ private static final String SPEC_REVISION = "revision";
+ private final SliceSpec mSpec;
/**
* @hide
@@ -76,7 +82,7 @@ public final class Slice {
@RestrictTo(Scope.LIBRARY)
@StringDef({HINT_TITLE, HINT_LIST, HINT_LIST_ITEM, HINT_LARGE, HINT_ACTIONS, HINT_SELECTED,
HINT_HORIZONTAL, HINT_NO_TINT, HINT_PARTIAL,
- SliceHints.HINT_HIDDEN, SliceHints.HINT_TOGGLE})
+ SliceHints.HINT_SUMMARY, SliceHints.SUBTYPE_TOGGLE})
public @interface SliceHint{ }
private final SliceItem[] mItems;
@@ -87,10 +93,12 @@ public final class Slice {
* @hide
*/
@RestrictTo(Scope.LIBRARY)
- Slice(ArrayList<SliceItem> items, @SliceHint String[] hints, Uri uri) {
+ Slice(ArrayList<SliceItem> items, @SliceHint String[] hints, Uri uri,
+ SliceSpec spec) {
mHints = hints;
mItems = items.toArray(new SliceItem[items.size()]);
mUri = uri;
+ mSpec = spec;
}
/**
@@ -107,6 +115,9 @@ public final class Slice {
}
}
mUri = in.getParcelable(URI);
+ mSpec = in.containsKey(SPEC_TYPE)
+ ? new SliceSpec(in.getString(SPEC_TYPE), in.getInt(SPEC_REVISION))
+ : null;
}
/**
@@ -122,10 +133,23 @@ public final class Slice {
}
b.putParcelableArray(ITEMS, p);
b.putParcelable(URI, mUri);
+ if (mSpec != null) {
+ b.putString(SPEC_TYPE, mSpec.getType());
+ b.putInt(SPEC_REVISION, mSpec.getRevision());
+ }
return b;
}
/**
+ * @return The spec for this slice
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public @Nullable SliceSpec getSpec() {
+ return mSpec;
+ }
+
+ /**
* @return The Uri that this Slice represents.
*/
public Uri getUri() {
@@ -156,12 +180,15 @@ public final class Slice {
/**
* A Builder used to construct {@link Slice}s
+ * @hide
*/
+ @RestrictTo(Scope.LIBRARY_GROUP)
public static class Builder {
private final Uri mUri;
private ArrayList<SliceItem> mItems = new ArrayList<>();
private @SliceHint ArrayList<String> mHints = new ArrayList<>();
+ private SliceSpec mSpec;
/**
* Create a builder which will construct a {@link Slice} for the Given Uri.
@@ -182,6 +209,16 @@ public final class Slice {
}
/**
+ * Add the spec for this slice.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Builder setSpec(SliceSpec spec) {
+ mSpec = spec;
+ return this;
+ }
+
+ /**
* Add hints to the Slice being constructed
*/
public Builder addHints(@SliceHint String... hints) {
@@ -289,24 +326,24 @@ public final class Slice {
}
/**
- * Add a color to the slice being constructed
+ * Add a int to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
*/
- public Builder addColor(int color, @Nullable String subType,
+ public Builder addInt(int value, @Nullable String subType,
@SliceHint String... hints) {
- mItems.add(new SliceItem(color, FORMAT_COLOR, subType, hints));
+ mItems.add(new SliceItem(value, FORMAT_INT, subType, hints));
return this;
}
/**
- * Add a color to the slice being constructed
+ * Add a int to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
*/
- public Builder addColor(int color, @Nullable String subType,
+ public Builder addInt(int value, @Nullable String subType,
@SliceHint List<String> hints) {
- return addColor(color, subType, hints.toArray(new String[hints.size()]));
+ return addInt(value, subType, hints.toArray(new String[hints.size()]));
}
/**
@@ -331,10 +368,20 @@ public final class Slice {
}
/**
+ * Add a SliceItem to the slice being constructed.
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
+ public Slice.Builder addItem(SliceItem item) {
+ mItems.add(item);
+ return this;
+ }
+
+ /**
* Construct the slice.
*/
public Slice build() {
- return new Slice(mItems, mHints.toArray(new String[mHints.size()]), mUri);
+ return new Slice(mItems, mHints.toArray(new String[mHints.size()]), mUri, mSpec);
}
}
@@ -355,6 +402,9 @@ public final class Slice {
if (FORMAT_SLICE.equals(mItems[i].getFormat())) {
sb.append("slice:\n");
sb.append(mItems[i].getSlice().toString(indent + " "));
+ } else if (FORMAT_ACTION.equals(mItems[i].getFormat())) {
+ sb.append("action:\n");
+ sb.append(mItems[i].getSlice().toString(indent + " "));
} else if (FORMAT_TEXT.equals(mItems[i].getFormat())) {
sb.append("text: ");
sb.append(mItems[i].getText());
@@ -377,17 +427,19 @@ public final class Slice {
*/
@SuppressWarnings("NewApi")
public static @Nullable Slice bindSlice(Context context, @NonNull Uri uri) {
+ // TODO: Hide this and only allow binding through SliceView.
if (BuildCompat.isAtLeastP()) {
- return callBindSlice(context, uri);
+ return callBindSlice(context, uri, Collections.<SliceSpec>emptyList());
} else {
- return SliceProviderCompat.bindSlice(context, uri);
+ return SliceProviderCompat.bindSlice(context, uri, Collections.<SliceSpec>emptyList());
}
}
@TargetApi(28)
- private static Slice callBindSlice(Context context, Uri uri) {
+ private static Slice callBindSlice(Context context, Uri uri,
+ List<SliceSpec> supportedSpecs) {
return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
- context.getContentResolver(), uri, SliceSpecs.SUPPORTED_SPECS));
+ context.getContentResolver(), uri, unwrap(supportedSpecs)));
}
@@ -405,16 +457,19 @@ public final class Slice {
*/
@SuppressWarnings("NewApi")
public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent) {
+ // TODO: Hide this and only allow binding through SliceView.
if (BuildCompat.isAtLeastP()) {
- return callBindSlice(context, intent);
+ return callBindSlice(context, intent, Collections.<SliceSpec>emptyList());
} else {
- return SliceProviderCompat.bindSlice(context, intent);
+ return SliceProviderCompat.bindSlice(context, intent,
+ Collections.<SliceSpec>emptyList());
}
}
@TargetApi(28)
- private static Slice callBindSlice(Context context, Intent intent) {
+ private static Slice callBindSlice(Context context, Intent intent,
+ List<SliceSpec> supportedSpecs) {
return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
- context, intent, SliceSpecs.SUPPORTED_SPECS));
+ context, intent, unwrap(supportedSpecs)));
}
}
diff --git a/androidx/app/slice/SliceConvert.java b/androidx/app/slice/SliceConvert.java
index edbc293a..0bacae7c 100644
--- a/androidx/app/slice/SliceConvert.java
+++ b/androidx/app/slice/SliceConvert.java
@@ -17,14 +17,18 @@ package androidx.app.slice;
import static android.app.slice.SliceItem.FORMAT_ACTION;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import android.support.annotation.RequiresApi;
+import android.support.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* Convert between {@link androidx.app.slice.Slice} and {@link android.app.slice.Slice}
@@ -39,6 +43,7 @@ public class SliceConvert {
android.app.slice.Slice.Builder builder = new android.app.slice.Slice.Builder(
slice.getUri());
builder.addHints(slice.getHints());
+ builder.setSpec(unwrap(slice.getSpec()));
for (androidx.app.slice.SliceItem item : slice.getItems()) {
switch (item.getFormat()) {
case FORMAT_SLICE:
@@ -57,8 +62,8 @@ public class SliceConvert {
case FORMAT_TEXT:
builder.addText(item.getText(), item.getSubType(), item.getHints());
break;
- case FORMAT_COLOR:
- builder.addColor(item.getColor(), item.getSubType(), item.getHints());
+ case FORMAT_INT:
+ builder.addInt(item.getInt(), item.getSubType(), item.getHints());
break;
case FORMAT_TIMESTAMP:
builder.addTimestamp(item.getTimestamp(), item.getSubType(), item.getHints());
@@ -68,6 +73,20 @@ public class SliceConvert {
return builder.build();
}
+ private static android.app.slice.SliceSpec unwrap(androidx.app.slice.SliceSpec spec) {
+ if (spec == null) return null;
+ return new android.app.slice.SliceSpec(spec.getType(), spec.getRevision());
+ }
+
+ static List<android.app.slice.SliceSpec> unwrap(
+ List<androidx.app.slice.SliceSpec> supportedSpecs) {
+ List<android.app.slice.SliceSpec> ret = new ArrayList<>();
+ for (androidx.app.slice.SliceSpec spec : supportedSpecs) {
+ ret.add(unwrap(spec));
+ }
+ return ret;
+ }
+
/**
* Convert {@link android.app.slice.Slice} to {@link androidx.app.slice.Slice}
*/
@@ -75,6 +94,7 @@ public class SliceConvert {
androidx.app.slice.Slice.Builder builder = new androidx.app.slice.Slice.Builder(
slice.getUri());
builder.addHints(slice.getHints());
+ builder.setSpec(wrap(slice.getSpec()));
for (android.app.slice.SliceItem item : slice.getItems()) {
switch (item.getFormat()) {
case FORMAT_SLICE:
@@ -93,8 +113,8 @@ public class SliceConvert {
case FORMAT_TEXT:
builder.addText(item.getText(), item.getSubType(), item.getHints());
break;
- case FORMAT_COLOR:
- builder.addColor(item.getColor(), item.getSubType(), item.getHints());
+ case FORMAT_INT:
+ builder.addInt(item.getInt(), item.getSubType(), item.getHints());
break;
case FORMAT_TIMESTAMP:
builder.addTimestamp(item.getTimestamp(), item.getSubType(), item.getHints());
@@ -103,4 +123,22 @@ public class SliceConvert {
}
return builder.build();
}
+
+ private static androidx.app.slice.SliceSpec wrap(android.app.slice.SliceSpec spec) {
+ if (spec == null) return null;
+ return new androidx.app.slice.SliceSpec(spec.getType(), spec.getRevision());
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static List<androidx.app.slice.SliceSpec> wrap(
+ List<android.app.slice.SliceSpec> supportedSpecs) {
+ List<androidx.app.slice.SliceSpec> ret = new ArrayList<>();
+ for (android.app.slice.SliceSpec spec : supportedSpecs) {
+ ret.add(wrap(spec));
+ }
+ return ret;
+ }
}
diff --git a/androidx/app/slice/SliceItem.java b/androidx/app/slice/SliceItem.java
index e4412d1f..3d58f3b5 100644
--- a/androidx/app/slice/SliceItem.java
+++ b/androidx/app/slice/SliceItem.java
@@ -17,8 +17,8 @@
package androidx.app.slice;
import static android.app.slice.SliceItem.FORMAT_ACTION;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
@@ -50,7 +50,7 @@ import java.util.List;
* <li>{@link android.app.slice.SliceItem#FORMAT_TEXT}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
- * <li>{@link android.app.slice.SliceItem#FORMAT_COLOR}</li>
+ * <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_TIMESTAMP}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}</li>
* <p>
@@ -70,7 +70,7 @@ public class SliceItem {
* @hide
*/
@RestrictTo(Scope.LIBRARY)
- @StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_COLOR,
+ @StringDef({FORMAT_SLICE, FORMAT_TEXT, FORMAT_IMAGE, FORMAT_ACTION, FORMAT_INT,
FORMAT_TIMESTAMP, FORMAT_REMOTE_INPUT})
public @interface SliceType {
}
@@ -138,7 +138,7 @@ public class SliceItem {
* <li>{@link android.app.slice.SliceItem#FORMAT_TEXT}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_IMAGE}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</li>
- * <li>{@link android.app.slice.SliceItem#FORMAT_COLOR}</li>
+ * <li>{@link android.app.slice.SliceItem#FORMAT_INT}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_TIMESTAMP}</li>
* <li>{@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}</li>
* @see #getSubType() ()
@@ -193,9 +193,9 @@ public class SliceItem {
}
/**
- * @return The color held by this {@link android.app.slice.SliceItem#FORMAT_COLOR} SliceItem
+ * @return The color held by this {@link android.app.slice.SliceItem#FORMAT_INT} SliceItem
*/
- public int getColor() {
+ public int getInt() {
return (Integer) mObj;
}
@@ -269,7 +269,7 @@ public class SliceItem {
* @hide
*/
@RestrictTo(Scope.LIBRARY)
- public boolean hasAnyHints(@Slice.SliceHint String[] hints) {
+ public boolean hasAnyHints(@Slice.SliceHint String... hints) {
if (hints == null) return false;
for (String hint : hints) {
if (ArrayUtils.contains(mHints, hint)) {
@@ -295,7 +295,7 @@ public class SliceItem {
case FORMAT_TEXT:
dest.putCharSequence(OBJ, (CharSequence) obj);
break;
- case FORMAT_COLOR:
+ case FORMAT_INT:
dest.putInt(OBJ, (Integer) mObj);
break;
case FORMAT_TIMESTAMP:
@@ -317,7 +317,7 @@ public class SliceItem {
return new Pair<>(
(PendingIntent) in.getParcelable(OBJ),
new Slice(in.getBundle(OBJ_2)));
- case FORMAT_COLOR:
+ case FORMAT_INT:
return in.getInt(OBJ);
case FORMAT_TIMESTAMP:
return in.getLong(OBJ);
@@ -339,8 +339,8 @@ public class SliceItem {
return "Image";
case FORMAT_ACTION:
return "Action";
- case FORMAT_COLOR:
- return "Color";
+ case FORMAT_INT:
+ return "Int";
case FORMAT_TIMESTAMP:
return "Timestamp";
case FORMAT_REMOTE_INPUT:
diff --git a/androidx/app/slice/SliceProvider.java b/androidx/app/slice/SliceProvider.java
index a0c12f12..8ec2dbef 100644
--- a/androidx/app/slice/SliceProvider.java
+++ b/androidx/app/slice/SliceProvider.java
@@ -24,8 +24,11 @@ import android.content.pm.ProviderInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
import android.support.v4.os.BuildCompat;
+import java.util.List;
+
import androidx.app.slice.compat.ContentProviderWrapper;
import androidx.app.slice.compat.SliceProviderCompat;
import androidx.app.slice.compat.SliceProviderWrapper;
@@ -71,6 +74,8 @@ import androidx.app.slice.compat.SliceProviderWrapper;
*/
public abstract class SliceProvider extends ContentProviderWrapper {
+ private static List<SliceSpec> sSpecs;
+
@Override
public void attachInfo(Context context, ProviderInfo info) {
ContentProvider impl;
@@ -127,4 +132,20 @@ public abstract class SliceProvider extends ContentProviderWrapper {
throw new UnsupportedOperationException(
"This provider has not implemented intent to uri mapping");
}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static void setSpecs(List<SliceSpec> specs) {
+ sSpecs = specs;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static List<SliceSpec> getCurrentSpecs() {
+ return sSpecs;
+ }
}
diff --git a/androidx/app/slice/SliceSpec.java b/androidx/app/slice/SliceSpec.java
new file mode 100644
index 00000000..0d7a157f
--- /dev/null
+++ b/androidx/app/slice/SliceSpec.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 androidx.app.slice;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+/**
+ * Class describing the structure of the data contained within a slice.
+ * <p>
+ * A data version contains a string which describes the type of structure
+ * and a revision which denotes this specific implementation. Revisions are expected
+ * to be backwards compatible and monotonically increasing. Meaning if a
+ * SliceSpec has the same type and an equal or lesser revision,
+ * it is expected to be compatible.
+ * <p>
+ * Apps rendering slices will provide a list of supported versions to the OS which
+ * will also be given to the app. Apps should only return a {@link Slice} with a
+ * {@link SliceSpec} that one of the supported {@link SliceSpec}s provided
+ * {@link #canRender}.
+ *
+ * @hide
+ * @see Slice
+ * @see SliceProvider#onBindSlice(Uri)
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SliceSpec {
+
+ private final String mType;
+ private final int mRevision;
+
+ public SliceSpec(@NonNull String type, int revision) {
+ mType = type;
+ mRevision = revision;
+ }
+
+ /**
+ * Gets the type of the version.
+ */
+ public String getType() {
+ return mType;
+ }
+
+ /**
+ * Gets the revision of the version.
+ */
+ public int getRevision() {
+ return mRevision;
+ }
+
+ /**
+ * Indicates that this spec can be used to render the specified spec.
+ * <p>
+ * Rendering support is not bi-directional (e.g. Spec v3 can render
+ * Spec v2, but Spec v2 cannot render Spec v3).
+ *
+ * @param candidate candidate format of data.
+ * @return true if versions are compatible.
+ * @see androidx.app.slice.widget.SliceView
+ */
+ public boolean canRender(@NonNull SliceSpec candidate) {
+ if (!mType.equals(candidate.mType)) return false;
+ return mRevision >= candidate.mRevision;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SliceSpec)) return false;
+ SliceSpec other = (SliceSpec) obj;
+ return mType.equals(other.mType) && mRevision == other.mRevision;
+ }
+}
diff --git a/androidx/app/slice/SliceTest.java b/androidx/app/slice/SliceTest.java
new file mode 100644
index 00000000..350c1770
--- /dev/null
+++ b/androidx/app/slice/SliceTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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 androidx.app.slice;
+
+import static android.app.slice.Slice.HINT_LARGE;
+import static android.app.slice.Slice.HINT_LIST;
+import static android.app.slice.Slice.HINT_NO_TINT;
+import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.SliceItem.FORMAT_ACTION;
+import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_SLICE;
+import static android.app.slice.SliceItem.FORMAT_TEXT;
+import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
+import static android.app.slice.SliceProvider.SLICE_TYPE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.PendingIntent.CanceledException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import androidx.app.slice.core.test.R;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SliceTest {
+
+ public static boolean sFlag = false;
+
+ private static final Uri BASE_URI = Uri.parse("content://androidx.app.slice.core.test/");
+ private final Context mContext = InstrumentationRegistry.getContext();
+
+ @Test
+ public void testProcess() {
+ sFlag = false;
+ Slice.bindSlice(mContext,
+ BASE_URI.buildUpon().appendPath("set_flag").build());
+ assertFalse(sFlag);
+ }
+
+ @Test
+ public void testType() {
+ assertEquals(SLICE_TYPE, mContext.getContentResolver().getType(BASE_URI));
+ }
+
+ @Test
+ public void testSliceUri() {
+ Slice s = Slice.bindSlice(mContext, BASE_URI);
+ assertEquals(BASE_URI, s.getUri());
+ }
+
+ @Test
+ public void testSubSlice() {
+ Uri uri = BASE_URI.buildUpon().appendPath("subslice").build();
+ Slice s = Slice.bindSlice(mContext, uri);
+ assertEquals(uri, s.getUri());
+ assertEquals(1, s.getItems().size());
+
+ SliceItem item = s.getItems().get(0);
+ assertEquals(FORMAT_SLICE, item.getFormat());
+ assertEquals("subslice", item.getSubType());
+ // The item should start with the same Uri as the parent, but be different.
+ assertTrue(item.getSlice().getUri().toString().startsWith(uri.toString()));
+ assertNotEquals(uri, item.getSlice().getUri());
+ }
+
+ @Test
+ public void testText() {
+ Uri uri = BASE_URI.buildUpon().appendPath("text").build();
+ Slice s = Slice.bindSlice(mContext, uri);
+ assertEquals(uri, s.getUri());
+ assertEquals(1, s.getItems().size());
+
+ SliceItem item = s.getItems().get(0);
+ assertEquals(FORMAT_TEXT, item.getFormat());
+ // TODO: Test spannables here.
+ assertEquals("Expected text", item.getText());
+ }
+
+ @Test
+ public void testIcon() {
+ Uri uri = BASE_URI.buildUpon().appendPath("icon").build();
+ Slice s = Slice.bindSlice(mContext, uri);
+ assertEquals(uri, s.getUri());
+ assertEquals(1, s.getItems().size());
+
+ SliceItem item = s.getItems().get(0);
+ assertEquals(FORMAT_IMAGE, item.getFormat());
+ assertEquals(Icon.createWithResource(mContext, R.drawable.size_48x48).toString(),
+ item.getIcon().toString());
+ }
+
+ @Test
+ public void testAction() {
+ sFlag = false;
+ final CountDownLatch latch = new CountDownLatch(1);
+ BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ sFlag = true;
+ latch.countDown();
+ }
+ };
+ mContext.registerReceiver(receiver,
+ new IntentFilter(mContext.getPackageName() + ".action"));
+ Uri uri = BASE_URI.buildUpon().appendPath("action").build();
+ Slice s = Slice.bindSlice(mContext, uri);
+ assertEquals(uri, s.getUri());
+ assertEquals(1, s.getItems().size());
+
+ SliceItem item = s.getItems().get(0);
+ assertEquals(FORMAT_ACTION, item.getFormat());
+ try {
+ item.getAction().send();
+ } catch (CanceledException e) {
+ }
+
+ try {
+ latch.await(100, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ assertTrue(sFlag);
+ mContext.unregisterReceiver(receiver);
+ }
+
+ @Test
+ public void testInt() {
+ Uri uri = BASE_URI.buildUpon().appendPath("int").build();
+ Slice s = Slice.bindSlice(mContext, uri);
+ assertEquals(uri, s.getUri());
+ assertEquals(1, s.getItems().size());
+
+ SliceItem item = s.getItems().get(0);
+ assertEquals(FORMAT_INT, item.getFormat());
+ assertEquals(0xff121212, item.getInt());
+ }
+
+ @Test
+ public void testTimestamp() {
+ Uri uri = BASE_URI.buildUpon().appendPath("timestamp").build();
+ Slice s = Slice.bindSlice(mContext, uri);
+ assertEquals(uri, s.getUri());
+ assertEquals(1, s.getItems().size());
+
+ SliceItem item = s.getItems().get(0);
+ assertEquals(FORMAT_TIMESTAMP, item.getFormat());
+ assertEquals(43, item.getTimestamp());
+ }
+
+ @Test
+ public void testHints() {
+ // Note this tests that hints are propagated through to the client but not that any specific
+ // hints have any effects.
+ Uri uri = BASE_URI.buildUpon().appendPath("hints").build();
+ Slice s = Slice.bindSlice(mContext, uri);
+ assertEquals(uri, s.getUri());
+
+ assertEquals(Arrays.asList(HINT_LIST), s.getHints());
+ assertEquals(Arrays.asList(HINT_TITLE), s.getItems().get(0).getHints());
+ assertEquals(Arrays.asList(HINT_NO_TINT, HINT_LARGE),
+ s.getItems().get(1).getHints());
+ }
+}
diff --git a/androidx/app/slice/SliceTestProvider.java b/androidx/app/slice/SliceTestProvider.java
new file mode 100644
index 00000000..9320bc97
--- /dev/null
+++ b/androidx/app/slice/SliceTestProvider.java
@@ -0,0 +1,75 @@
+/*
+ * 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 androidx.app.slice;
+
+import static android.app.slice.Slice.HINT_LARGE;
+import static android.app.slice.Slice.HINT_LIST;
+import static android.app.slice.Slice.HINT_NO_TINT;
+import static android.app.slice.Slice.HINT_TITLE;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+
+import androidx.app.slice.Slice.Builder;
+import androidx.app.slice.core.test.R;
+
+public class SliceTestProvider extends androidx.app.slice.SliceProvider {
+
+ @Override
+ public boolean onCreateSliceProvider() {
+ return true;
+ }
+
+ @Override
+ public Slice onBindSlice(Uri sliceUri) {
+ switch (sliceUri.getPath()) {
+ case "/set_flag":
+ SliceTest.sFlag = true;
+ break;
+ case "/subslice":
+ Builder b = new Builder(sliceUri);
+ return b.addSubSlice(new Slice.Builder(b).build(), "subslice").build();
+ case "/text":
+ return new Slice.Builder(sliceUri).addText("Expected text", "text").build();
+ case "/icon":
+ return new Slice.Builder(sliceUri).addIcon(
+ Icon.createWithResource(getContext(), R.drawable.size_48x48),
+ "icon").build();
+ case "/action":
+ Builder builder = new Builder(sliceUri);
+ Slice subSlice = new Slice.Builder(builder).build();
+ PendingIntent broadcast = PendingIntent.getBroadcast(getContext(), 0,
+ new Intent(getContext().getPackageName() + ".action"), 0);
+ return builder.addAction(broadcast, subSlice, "action").build();
+ case "/int":
+ return new Slice.Builder(sliceUri).addInt(0xff121212, "int").build();
+ case "/timestamp":
+ return new Slice.Builder(sliceUri).addTimestamp(43, "timestamp").build();
+ case "/hints":
+ return new Slice.Builder(sliceUri)
+ .addHints(HINT_LIST)
+ .addText("Text", null, HINT_TITLE)
+ .addIcon(Icon.createWithResource(getContext(), R.drawable.size_48x48),
+ null, HINT_NO_TINT, HINT_LARGE)
+ .build();
+ }
+ return new Slice.Builder(sliceUri).build();
+ }
+
+}
diff --git a/androidx/app/slice/SliceUtils.java b/androidx/app/slice/SliceUtils.java
new file mode 100644
index 00000000..ededbfdd
--- /dev/null
+++ b/androidx/app/slice/SliceUtils.java
@@ -0,0 +1,158 @@
+/*
+ * 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 androidx.app.slice;
+
+import static android.app.slice.SliceItem.FORMAT_ACTION;
+import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
+
+import android.content.Context;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Utilities for dealing with slices.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SliceUtils {
+
+ private SliceUtils() {
+ }
+
+ /**
+ * Serialize a slice to an OutputStream.
+ * <p>
+ * The slice can later be read into slice form again with {@link #parseSlice}.
+ * Some slice types cannot be serialized, their handling is controlled by
+ * {@link SerializeOptions}.
+ *
+ * @param s The slice to serialize.
+ * @param context Context used to load any resources in the slice.
+ * @param output The output of the serialization.
+ * @param encoding The encoding to use for serialization.
+ * @param options Options defining how to handle non-serializable items.
+ */
+ public static void serializeSlice(@NonNull Slice s, @NonNull Context context,
+ @NonNull OutputStream output, @NonNull String encoding,
+ @NonNull SerializeOptions options) throws IOException {
+ SliceXml.serializeSlice(s, context, output, encoding, options);
+ }
+
+ /**
+ * Parse a slice that has been previously serialized.
+ * <p>
+ * Parses a slice that was serialized with {@link #serializeSlice}.
+ *
+ * @param input The input stream to read from.
+ * @param encoding The encoding to read as.
+ */
+ public static @NonNull Slice parseSlice(@NonNull InputStream input, @NonNull String encoding)
+ throws IOException {
+ return SliceXml.parseSlice(input, encoding);
+ }
+
+ /**
+ * Holds options for how to handle SliceItems that cannot be serialized.
+ */
+ public static class SerializeOptions {
+ /**
+ * Constant indicating that the an {@link IllegalArgumentException} should be thrown
+ * when this format is encountered.
+ */
+ public static final int MODE_THROW = 0;
+ /**
+ * Constant indicating that the SliceItem should be removed when this format is encountered.
+ */
+ public static final int MODE_REMOVE = 1;
+ /**
+ * Constant indicating that the SliceItem should be serialized as much as possible.
+ * <p>
+ * For images this means it will be replaced with an empty image. For actions, the
+ * action will be removed but the content of the action will be serialized.
+ */
+ public static final int MODE_DISABLE = 2;
+
+ @IntDef({MODE_THROW, MODE_REMOVE, MODE_DISABLE})
+ @interface FormatMode {
+ }
+
+ private int mActionMode = MODE_THROW;
+ private int mImageMode = MODE_THROW;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public void checkThrow(String format) {
+ switch (format) {
+ case FORMAT_ACTION:
+ case FORMAT_REMOTE_INPUT:
+ if (mActionMode != MODE_THROW) return;
+ break;
+ case FORMAT_IMAGE:
+ if (mImageMode != MODE_THROW) return;
+ break;
+ default:
+ return;
+ }
+ throw new IllegalArgumentException(format + " cannot be serialized");
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public @FormatMode int getActionMode() {
+ return mActionMode;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public @FormatMode int getImageMode() {
+ return mImageMode;
+ }
+
+ /**
+ * Sets how {@link android.app.slice.SliceItem#FORMAT_ACTION} items should be handled.
+ *
+ * The default mode is {@link #MODE_THROW}.
+ * @param mode The desired mode.
+ */
+ public SerializeOptions setActionMode(@FormatMode int mode) {
+ mActionMode = mode;
+ return this;
+ }
+
+ /**
+ * Sets how {@link android.app.slice.SliceItem#FORMAT_IMAGE} items should be handled.
+ *
+ * The default mode is {@link #MODE_THROW}.
+ * @param mode The desired mode.
+ */
+ public SerializeOptions setImageMode(@FormatMode int mode) {
+ mImageMode = mode;
+ return this;
+ }
+ }
+}
diff --git a/androidx/app/slice/SliceXml.java b/androidx/app/slice/SliceXml.java
new file mode 100644
index 00000000..2500ef6d
--- /dev/null
+++ b/androidx/app/slice/SliceXml.java
@@ -0,0 +1,244 @@
+/*
+ * 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 androidx.app.slice;
+
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
+import static org.xmlpull.v1.XmlPullParser.TEXT;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.support.annotation.RestrictTo;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SliceXml {
+
+ private static final String NAMESPACE = null;
+
+ private static final String TAG_SLICE = "slice";
+ private static final String TAG_ITEM = "item";
+
+ private static final String ATTR_URI = "uri";
+ private static final String ATTR_FORMAT = "format";
+ private static final String ATTR_SUBTYPE = "subtype";
+ private static final String ATTR_HINTS = "hints";
+
+ public static Slice parseSlice(InputStream input, String encoding) throws IOException {
+ try {
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(input, encoding);
+
+ int outerDepth = parser.getDepth();
+ int type;
+ Slice s = null;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type != START_TAG) {
+ continue;
+ }
+ s = parseSlice(parser);
+ }
+ return s;
+ } catch (XmlPullParserException e) {
+ throw new IOException("Unable to init XML Serialization", e);
+ }
+ }
+
+ @SuppressLint("WrongConstant")
+ private static Slice parseSlice(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ if (!TAG_SLICE.equals(parser.getName())) {
+ throw new IOException("Unexpected tag " + parser.getName());
+ }
+ int outerDepth = parser.getDepth();
+ int type;
+ String uri = parser.getAttributeValue(NAMESPACE, ATTR_URI);
+ Slice.Builder b = new Slice.Builder(Uri.parse(uri));
+ String[] hints = hints(parser.getAttributeValue(NAMESPACE, ATTR_HINTS));
+ b.addHints(hints);
+
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == START_TAG && TAG_ITEM.equals(parser.getName())) {
+ parseItem(b, parser);
+ }
+ }
+ return b.build();
+ }
+
+ @SuppressLint("WrongConstant")
+ private static void parseItem(Slice.Builder b, XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ int type;
+ int outerDepth = parser.getDepth();
+ String format = parser.getAttributeValue(NAMESPACE, ATTR_FORMAT);
+ String subtype = parser.getAttributeValue(NAMESPACE, ATTR_SUBTYPE);
+ String hintStr = parser.getAttributeValue(NAMESPACE, ATTR_HINTS);
+ String[] hints = hints(hintStr);
+ String v;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == TEXT) {
+ switch (format) {
+ case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT:
+ // Nothing for now.
+ break;
+ case android.app.slice.SliceItem.FORMAT_IMAGE:
+ v = parser.getText();
+ if (!TextUtils.isEmpty(v)) {
+ if (android.os.Build.VERSION.SDK_INT
+ >= android.os.Build.VERSION_CODES.M) {
+ String[] split = v.split(",");
+ int w = Integer.parseInt(split[0]);
+ int h = Integer.parseInt(split[1]);
+ Bitmap image = Bitmap.createBitmap(w, h, Bitmap.Config.ALPHA_8);
+ b.addIcon(Icon.createWithBitmap(image), subtype, hints);
+ }
+ }
+ break;
+ case android.app.slice.SliceItem.FORMAT_INT:
+ v = parser.getText();
+ b.addInt(Integer.parseInt(v), subtype, hints);
+ break;
+ case android.app.slice.SliceItem.FORMAT_TEXT:
+ v = parser.getText();
+ b.addText(Html.fromHtml(v), subtype, hints);
+ break;
+ case android.app.slice.SliceItem.FORMAT_TIMESTAMP:
+ v = parser.getText();
+ b.addTimestamp(Long.parseLong(v), subtype, hints);
+ break;
+ default:
+ throw new IllegalArgumentException("Unrecognized format " + format);
+ }
+ } else if (type == START_TAG && TAG_SLICE.equals(parser.getName())) {
+ b.addSubSlice(parseSlice(parser), subtype);
+ }
+ }
+ }
+
+ private static String[] hints(String hintStr) {
+ return TextUtils.isEmpty(hintStr) ? new String[0] : hintStr.split(",");
+ }
+
+ public static void serializeSlice(Slice s, Context context, OutputStream output,
+ String encoding, SliceUtils.SerializeOptions options) throws IOException {
+ try {
+ XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer();
+ serializer.setOutput(output, encoding);
+ serializer.startDocument(encoding, null);
+
+ serialize(s, context, options, serializer);
+
+ serializer.endDocument();
+ serializer.flush();
+ } catch (XmlPullParserException e) {
+ throw new IOException("Unable to init XML Serialization", e);
+ }
+ }
+
+ private static void serialize(Slice s, Context context, SliceUtils.SerializeOptions options,
+ XmlSerializer serializer) throws IOException {
+ serializer.startTag(NAMESPACE, TAG_SLICE);
+ serializer.attribute(NAMESPACE, ATTR_URI, s.getUri().toString());
+ if (!s.getHints().isEmpty()) {
+ serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(s.getHints()));
+ }
+ for (SliceItem item : s.getItems()) {
+ serialize(item, context, options, serializer);
+ }
+
+ serializer.endTag(NAMESPACE, TAG_SLICE);
+ }
+
+ private static void serialize(SliceItem item, Context context,
+ SliceUtils.SerializeOptions options, XmlSerializer serializer) throws IOException {
+ String format = item.getFormat();
+ options.checkThrow(format);
+
+ serializer.startTag(NAMESPACE, TAG_ITEM);
+ serializer.attribute(NAMESPACE, ATTR_FORMAT, format);
+ if (item.getSubType() != null) {
+ serializer.attribute(NAMESPACE, ATTR_SUBTYPE, item.getSubType());
+ }
+ if (!item.getHints().isEmpty()) {
+ serializer.attribute(NAMESPACE, ATTR_HINTS, hintStr(item.getHints()));
+ }
+
+ switch (format) {
+ case android.app.slice.SliceItem.FORMAT_ACTION:
+ if (options.getActionMode() == SliceUtils.SerializeOptions.MODE_DISABLE) {
+ serialize(item.getSlice(), context, options, serializer);
+ }
+ break;
+ case android.app.slice.SliceItem.FORMAT_REMOTE_INPUT:
+ // Nothing for now.
+ break;
+ case android.app.slice.SliceItem.FORMAT_IMAGE:
+ if (options.getImageMode() == SliceUtils.SerializeOptions.MODE_DISABLE) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+ Drawable d = item.getIcon().loadDrawable(context);
+ serializer.text(String.format("%d,%d",
+ d.getIntrinsicWidth(), d.getIntrinsicHeight()));
+ }
+ }
+ break;
+ case android.app.slice.SliceItem.FORMAT_INT:
+ serializer.text(String.valueOf(item.getInt()));
+ break;
+ case android.app.slice.SliceItem.FORMAT_SLICE:
+ serialize(item.getSlice(), context, options, serializer);
+ break;
+ case android.app.slice.SliceItem.FORMAT_TEXT:
+ if (item.getText() instanceof Spanned) {
+ serializer.text(Html.toHtml((Spanned) item.getText()));
+ } else {
+ serializer.text(String.valueOf(item.getText()));
+ }
+ break;
+ case android.app.slice.SliceItem.FORMAT_TIMESTAMP:
+ serializer.text(String.valueOf(item.getTimestamp()));
+ break;
+ default:
+ throw new IllegalArgumentException("Unrecognized format " + format);
+ }
+ serializer.endTag(NAMESPACE, TAG_ITEM);
+ }
+
+ private static String hintStr(List<String> hints) {
+ return TextUtils.join(",", hints);
+ }
+}
diff --git a/androidx/app/slice/SliceXmlTest.java b/androidx/app/slice/SliceXmlTest.java
new file mode 100644
index 00000000..5e4444d0
--- /dev/null
+++ b/androidx/app/slice/SliceXmlTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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 androidx.app.slice;
+
+
+import static android.app.slice.SliceItem.FORMAT_ACTION;
+import static android.app.slice.SliceItem.FORMAT_SLICE;
+import static android.app.slice.SliceItem.FORMAT_TEXT;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SliceXmlTest {
+
+ private final Context mContext = InstrumentationRegistry.getContext();
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testThrowForAction() throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
+ .addAction(null, null, null)
+ .build();
+ SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
+ .SerializeOptions());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testThrowForRemoteInput() throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
+ .addRemoteInput(null, null)
+ .build();
+ SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
+ .SerializeOptions());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testThrowForImage() throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
+ .addIcon(null, null)
+ .build();
+ SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
+ .SerializeOptions());
+ }
+
+ @Test
+ public void testNoThrowForAction() throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
+ .addAction(null, null, null)
+ .build();
+ SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
+ .SerializeOptions().setActionMode(SliceUtils.SerializeOptions.MODE_REMOVE));
+ }
+
+ @Test
+ public void testNoThrowForRemoteInput() throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
+ .addRemoteInput(null, null)
+ .build();
+ SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
+ .SerializeOptions().setActionMode(SliceUtils.SerializeOptions.MODE_REMOVE));
+ }
+
+ @Test
+ public void testNoThrowForImage() throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ Slice s = new Slice.Builder(Uri.parse("content://pkg/slice"))
+ .addIcon(null, null)
+ .build();
+ SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8", new SliceUtils
+ .SerializeOptions().setImageMode(SliceUtils.SerializeOptions.MODE_REMOVE));
+ }
+
+ @Test
+ public void testSerialization() throws IOException {
+ Bitmap b = Bitmap.createBitmap(50, 25, Bitmap.Config.ARGB_8888);
+ new Canvas(b).drawColor(0xffff0000);
+ // Create a slice containing all the types in a hierarchy.
+ Slice before = new Slice.Builder(Uri.parse("content://pkg/slice"))
+ .addSubSlice(new Slice.Builder(Uri.parse("content://pkg/slice/sub"))
+ .addTimestamp(System.currentTimeMillis(), null, "Hint")
+ .build())
+ .addIcon(Icon.createWithBitmap(b), null)
+ .addText("Some text", null)
+ .addAction(null, new Slice.Builder(Uri.parse("content://pkg/slice/sub"))
+ .addText("Action text", null)
+ .build(), null)
+ .addInt(0xff00ff00, "subtype")
+ .addHints("Hint 1", "Hint 2")
+ .build();
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ SliceUtils.serializeSlice(before, mContext, outputStream, "UTF-8",
+ new SliceUtils.SerializeOptions()
+ .setImageMode(SliceUtils.SerializeOptions.MODE_DISABLE)
+ .setActionMode(SliceUtils.SerializeOptions.MODE_DISABLE));
+
+ byte[] bytes = outputStream.toByteArray();
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+ Slice after = SliceUtils.parseSlice(inputStream, "UTF-8");
+
+ assertEquivalent(before, after);
+ }
+
+ private void assertEquivalent(Slice desired, Slice actual) {
+ assertEquals(desired.getUri(), actual.getUri());
+ assertEquals(desired.getHints(), actual.getHints());
+ assertEquals(desired.getItems().size(), actual.getItems().size());
+
+ for (int i = 0; i < desired.getItems().size(); i++) {
+ assertEquivalent(desired.getItems().get(i), actual.getItems().get(i));
+ }
+ }
+
+ private void assertEquivalent(SliceItem desired, SliceItem actual) {
+ boolean isSliceType = FORMAT_SLICE.equals(desired.getFormat())
+ || FORMAT_ACTION.equals(desired.getFormat());
+ if (isSliceType) {
+ assertTrue(FORMAT_SLICE.equals(actual.getFormat())
+ || FORMAT_ACTION.equals(actual.getFormat()));
+ } else {
+ assertEquals(desired.getFormat(), actual.getFormat());
+ if (FORMAT_TEXT.equals(desired.getFormat())) {
+ assertEquals(String.valueOf(desired.getText()), String.valueOf(actual.getText()));
+ }
+ }
+ }
+}
diff --git a/androidx/app/slice/builders/GridBuilder.java b/androidx/app/slice/builders/GridBuilder.java
new file mode 100644
index 00000000..f51b0263
--- /dev/null
+++ b/androidx/app/slice/builders/GridBuilder.java
@@ -0,0 +1,209 @@
+/*
+ * 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 androidx.app.slice.builders;
+
+import static android.app.slice.Slice.HINT_HORIZONTAL;
+import static android.app.slice.Slice.HINT_LARGE;
+import static android.app.slice.Slice.HINT_LIST_ITEM;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.RestrictTo;
+
+import java.util.function.Consumer;
+
+import androidx.app.slice.Slice;
+
+/**
+ * Builder to construct a row of slice content in a grid format.
+ * <p>
+ * A grid row is composed of cells, each cell can have a combination of text and images. For more
+ * details see {@link CellBuilder}.
+ * </p>
+ */
+public class GridBuilder extends TemplateSliceBuilder {
+
+ /**
+ * Create a builder which will construct a slice displayed in a grid format.
+ * @param uri Uri to tag for this slice.
+ */
+ public GridBuilder(@NonNull Uri uri) {
+ super(new Slice.Builder(uri));
+ }
+
+ /**
+ * Create a builder which will construct a slice displayed in a grid format.
+ * @param parent The builder constructing the parent slice.
+ */
+ public GridBuilder(@NonNull TemplateSliceBuilder parent) {
+ super(new Slice.Builder(parent.getBuilder()));
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(Slice.Builder builder) {
+ }
+
+ @Override
+ @NonNull
+ public Slice build() {
+ return new Slice.Builder(getBuilder()).addHints(HINT_HORIZONTAL, HINT_LIST_ITEM)
+ .addSubSlice(getBuilder()
+ .addHints(HINT_HORIZONTAL, HINT_LIST_ITEM).build()).build();
+ }
+
+ /**
+ * Add a cell to the grid builder.
+ */
+ @NonNull
+ public GridBuilder addCell(@NonNull CellBuilder builder) {
+ getBuilder().addSubSlice(builder.build());
+ return this;
+ }
+
+ /**
+ * Add a cell to the grid builder.
+ */
+ @RequiresApi(Build.VERSION_CODES.N)
+ @NonNull
+ public GridBuilder addCell(@NonNull Consumer<CellBuilder> c) {
+ CellBuilder b = new CellBuilder(this);
+ c.accept(b);
+ return addCell(b);
+ }
+
+ /**
+ * Sub-builder to construct a cell to be displayed in a grid.
+ * <p>
+ * Content added to a cell will be displayed in order vertically, for example the below code
+ * would construct a cell with "First text", and image below it, and then "Second text" below
+ * the image.
+ *
+ * <pre class="prettyprint">
+ * CellBuilder cb = new CellBuilder(sliceUri);
+ * cb.addText("First text")
+ * .addImage(middleIcon)
+ * .addText("Second text");
+ * </pre>
+ *
+ * A cell can have at most two text items and one image.
+ * </p>
+ */
+ public static final class CellBuilder extends TemplateSliceBuilder {
+
+ private PendingIntent mContentIntent;
+
+ /**
+ * Create a builder which will construct a slice displayed as a cell in a grid.
+ * @param parent The builder constructing the parent slice.
+ */
+ public CellBuilder(@NonNull GridBuilder parent) {
+ super(parent.createChildBuilder());
+ }
+
+ /**
+ * Create a builder which will construct a slice displayed as a cell in a grid.
+ * @param uri Uri to tag for this slice.
+ */
+ public CellBuilder(@NonNull Uri uri) {
+ super(new Slice.Builder(uri));
+ }
+
+ /**
+ * Adds text to the cell. There can be at most two text items, the first two added
+ * will be used, others will be ignored.
+ */
+ @NonNull
+ public CellBuilder addText(@NonNull CharSequence text) {
+ getBuilder().addText(text, null);
+ return this;
+ }
+
+ /**
+ * Adds text to the cell. Text added with this method will be styled as a title.
+ * There can be at most two text items, the first two added will be used, others
+ * will be ignored.
+ */
+ @NonNull
+ public CellBuilder addTitleText(@NonNull CharSequence text) {
+ getBuilder().addText(text, null, HINT_LARGE);
+ return this;
+ }
+
+ /**
+ * Adds an image to the cell that should be displayed as large as the cell allows.
+ * There can be at most one image, the first one added will be used, others will be ignored.
+ *
+ * @param image the image to display in the cell.
+ */
+ @NonNull
+ public CellBuilder addLargeImage(@NonNull Icon image) {
+ getBuilder().addIcon(image, null, HINT_LARGE);
+ return this;
+ }
+
+ /**
+ * Adds an image to the cell. There can be at most one image, the first one added
+ * will be used, others will be ignored.
+ *
+ * @param image the image to display in the cell.
+ */
+ @NonNull
+ public CellBuilder addImage(@NonNull Icon image) {
+ getBuilder().addIcon(image, null);
+ return this;
+ }
+
+ /**
+ * Sets the action to be invoked if the user taps on this cell in the row.
+ */
+ @NonNull
+ public CellBuilder setContentIntent(@NonNull PendingIntent intent) {
+ mContentIntent = intent;
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @Override
+ public void apply(Slice.Builder b) {
+ }
+
+ @Override
+ @NonNull
+ public Slice build() {
+ if (mContentIntent != null) {
+ return new Slice.Builder(getBuilder())
+ .addHints(HINT_HORIZONTAL, HINT_LIST_ITEM)
+ .addAction(mContentIntent, getBuilder().build(), null)
+ .build();
+ }
+ return getBuilder().addHints(HINT_HORIZONTAL, HINT_LIST_ITEM).build();
+ }
+ }
+}
diff --git a/androidx/app/slice/builders/ListBuilder.java b/androidx/app/slice/builders/ListBuilder.java
new file mode 100644
index 00000000..2cb75d72
--- /dev/null
+++ b/androidx/app/slice/builders/ListBuilder.java
@@ -0,0 +1,431 @@
+/*
+ * 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 androidx.app.slice.builders;
+
+import static android.app.slice.Slice.HINT_LARGE;
+import static android.app.slice.Slice.HINT_LIST_ITEM;
+import static android.app.slice.Slice.HINT_NO_TINT;
+import static android.app.slice.Slice.HINT_SELECTED;
+import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.Slice.SUBTYPE_COLOR;
+import static android.app.slice.SliceItem.FORMAT_ACTION;
+import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_TEXT;
+import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import static androidx.app.slice.core.SliceHints.HINT_SUMMARY;
+import static androidx.app.slice.core.SliceHints.SUBTYPE_TOGGLE;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceItem;
+import androidx.app.slice.core.SliceHints;
+
+/**
+ * Builder to construct slice content in a list format.
+ * <p>
+ * Use this builder for showing rows of content which is composed of text, images, and actions. For
+ * more details see {@link RowBuilder}.
+ * </p>
+ * <p>
+ * Slices can be displayed in different formats:
+ * <ul>
+ * <li>Shortcut - The slice is displayed as an icon with a text label.</li>
+ * <li>Small - Only a single row of content is displayed in small format, to specify which
+ * row to display in small format see {@link #addSummaryRow(RowBuilder)}.</li>
+ * <li>Large - As many rows of content are shown as possible. If the presenter of the slice
+ * allows scrolling then all rows of content will be displayed in a scrollable view.</li>
+ * </ul>
+ * </p>
+ *
+ * @see RowBuilder
+ */
+public class ListBuilder extends TemplateSliceBuilder {
+
+ private boolean mHasSummary;
+
+ /**
+ * Create a builder which will construct a slice that will display rows of content.
+ * @param uri Uri to tag for this slice.
+ */
+ public ListBuilder(@NonNull Uri uri) {
+ super(uri);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(androidx.app.slice.Slice.Builder builder) {
+
+ }
+
+ /**
+ * Add a row to list builder.
+ */
+ @NonNull
+ public ListBuilder addRow(@NonNull RowBuilder builder) {
+ getBuilder().addSubSlice(builder.build());
+ return this;
+ }
+
+ /**
+ * Add a row the list builder.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ @NonNull
+ public ListBuilder addRow(@NonNull Consumer<RowBuilder> c) {
+ RowBuilder b = new RowBuilder(this);
+ c.accept(b);
+ return addRow(b);
+
+ }
+
+ /**
+ * Add a grid row to the list builder.
+ */
+ @NonNull
+ public ListBuilder addGrid(@NonNull GridBuilder builder) {
+ getBuilder().addSubSlice(builder.build());
+ return this;
+ }
+
+ /**
+ * Add a grid row to the list builder.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ @NonNull
+ public ListBuilder addGrid(@NonNull Consumer<GridBuilder> c) {
+ GridBuilder b = new GridBuilder(this);
+ c.accept(b);
+ return addGrid(b);
+ }
+
+ /**
+ * Add a summary row for this template. The summary content is displayed
+ * when the slice is displayed in small format.
+ * <p>
+ * Only one summary row can be added, this throws {@link IllegalArgumentException} if
+ * called more than once.
+ * </p>
+ */
+ public ListBuilder addSummaryRow(RowBuilder builder) {
+ if (mHasSummary) {
+ throw new IllegalArgumentException("Trying to add summary row when one has "
+ + "already been added");
+ }
+ builder.getBuilder().addHints(HINT_SUMMARY);
+ getBuilder().addSubSlice(builder.build(), null);
+ mHasSummary = true;
+ return this;
+ }
+
+ /**
+ * Add a summary row for this template. The summary content is displayed
+ * when the slice is displayed in small format.
+ * <p>
+ * Only one summary row can be added, this throws {@link IllegalArgumentException} if
+ * called more than once.
+ * </p>
+ */
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public ListBuilder addSummaryRow(Consumer<RowBuilder> c) {
+ if (mHasSummary) {
+ throw new IllegalArgumentException("Trying to add summary row when one has "
+ + "already been added");
+ }
+ RowBuilder b = new RowBuilder(this);
+ c.accept(b);
+ b.getBuilder().addHints(HINT_SUMMARY);
+ getBuilder().addSubSlice(b.build(), null);
+ mHasSummary = true;
+ return this;
+ }
+
+ /**
+ * Sets the color to tint items displayed by this template (e.g. icons).
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @NonNull
+ public ListBuilder setColor(int color) {
+ getBuilder().addInt(color, SUBTYPE_COLOR);
+ return this;
+ }
+
+ /**
+ * Sub-builder to construct a row of slice content.
+ * <p>
+ * Row content can have:
+ * <ul>
+ * <li>Title item - This is only displayed if this is a list item in a large template, it
+ * will not be shown if this template is being used for small format. The item appears at
+ * the start of the template. There can only be one title item displayed, and it could be a
+ * timestamp, image, or a tappable icon.</li>
+ * <li>Title - Formatted as a title.</li>
+ * <li>Subtitle - Appears below the title (if one exists) and is formatted as normal text.
+ * </li>
+ * <li>End item - Appears at the end of the template, there can be multiple end items but
+ * they are only shown if there's enough space. End items can be a timestamp, image, or a
+ * tappable icon.</li>
+ * </ul>
+ * </p>
+ *
+ * @see ListBuilder
+ */
+ public static class RowBuilder extends TemplateSliceBuilder {
+
+ private boolean mIsHeader;
+ private PendingIntent mContentIntent;
+ private SliceItem mTitleItem;
+ private SliceItem mSubtitleItem;
+ private SliceItem mStartItem;
+ private ArrayList<SliceItem> mEndItems = new ArrayList<>();
+ private boolean mHasToggle;
+ private boolean mHasEndAction;
+ private boolean mHasEndImage;
+ private boolean mHasTimestamp;
+
+ /**
+ * Create a builder which will construct a slice displayed in a row format.
+ * @param parent The builder constructing the parent slice.
+ */
+ public RowBuilder(@NonNull ListBuilder parent) {
+ super(parent.createChildBuilder());
+ }
+
+ /**
+ * Create a builder which will construct a slice displayed in a row format.
+ * @param uri Uri to tag for this slice.
+ */
+ public RowBuilder(@NonNull Uri uri) {
+ super(new Slice.Builder(uri));
+ }
+
+ /**
+ * Sets this row to be the header of the slice. This item will be displayed at the top of
+ * the slice and other items in the slice will scroll below it.
+ */
+ @NonNull
+ public RowBuilder setIsHeader(boolean isHeader) {
+ mIsHeader = isHeader;
+ return this;
+ }
+
+ /**
+ * Sets the title item to be the provided timestamp. Only one timestamp can be added, if
+ * one is already added this will throw {@link IllegalArgumentException}.
+ * <p>
+ * There can only be one title item, this will replace any other title
+ * items that may have been set.
+ */
+ @NonNull
+ public RowBuilder setTitleItem(long timeStamp) {
+ if (mHasTimestamp) {
+ throw new IllegalArgumentException("Trying to add a timestamp when one has "
+ + "already been added");
+ }
+ mStartItem = new SliceItem(timeStamp, FORMAT_TIMESTAMP, null, new String[0]);
+ mHasTimestamp = true;
+ return this;
+ }
+
+ /**
+ * Sets the title item to be the provided icon.
+ * <p>
+ * There can only be one title item, this will replace any other title
+ * items that may have been set.
+ */
+ @NonNull
+ public RowBuilder setTitleItem(@NonNull Icon icon) {
+ mStartItem = new SliceItem(icon, FORMAT_IMAGE, null, new String[0]);
+ return this;
+ }
+
+ /**
+ * Sets the title item to be a tappable icon.
+ * <p>
+ * There can only be one title item, this will replace any other title
+ * items that may have been set.
+ */
+ @NonNull
+ public RowBuilder setTitleItem(@NonNull Icon icon, @NonNull PendingIntent action) {
+ Slice actionSlice = new Slice.Builder(getBuilder()).addIcon(icon, null).build();
+ mStartItem = new SliceItem(action, actionSlice, FORMAT_ACTION, null, new String[0]);
+ return this;
+ }
+
+ /**
+ * Sets the action to be invoked if the user taps on the main content of the template.
+ */
+ @NonNull
+ public RowBuilder setContentIntent(@NonNull PendingIntent action) {
+ mContentIntent = action;
+ return this;
+ }
+
+ /**
+ * Sets the title text.
+ */
+ @NonNull
+ public RowBuilder setTitle(CharSequence title) {
+ mTitleItem = new SliceItem(title, FORMAT_TEXT, null, new String[] {HINT_TITLE});
+ return this;
+ }
+
+ /**
+ * Sets the subtitle text.
+ */
+ @NonNull
+ public RowBuilder setSubtitle(CharSequence subtitle) {
+ mSubtitleItem = new SliceItem(subtitle, FORMAT_TEXT, null, new String[0]);
+ return this;
+ }
+
+ /**
+ * Adds a timestamp to be displayed at the end of the row. Only one timestamp can be added,
+ * if one is already added this will throw {@link IllegalArgumentException}.
+ */
+ @NonNull
+ public RowBuilder addEndItem(long timeStamp) {
+ if (mHasTimestamp) {
+ throw new IllegalArgumentException("Trying to add a timestamp when one has "
+ + "already been added");
+ }
+ mEndItems.add(new SliceItem(timeStamp, FORMAT_TIMESTAMP, null, new String[0]));
+ mHasTimestamp = true;
+ return this;
+ }
+
+ /**
+ * Adds an icon to be displayed at the end of the row. A mixture of icons and tappable
+ * icons is not permitted, if an action has already been added this will throw
+ * {@link IllegalArgumentException}.
+ */
+ @NonNull
+ public RowBuilder addEndItem(@NonNull Icon icon) {
+ if (mHasEndAction) {
+ throw new IllegalArgumentException("Trying to add an icon to end items when an"
+ + "action has already been added. End items cannot have a mixture of "
+ + "tappable icons and icons.");
+ }
+ mEndItems.add(new SliceItem(icon, FORMAT_IMAGE, null,
+ new String[] {HINT_NO_TINT, HINT_LARGE}));
+ mHasEndImage = true;
+ return this;
+ }
+
+ /**
+ * Adds a tappable icon to be displayed at the end of the row. A mixture of icons and
+ * tappable icons is not permitted, if an icon has already been added this will throw
+ * {@link IllegalArgumentException}.
+ */
+ @NonNull
+ public RowBuilder addEndItem(@NonNull Icon icon, @NonNull PendingIntent action) {
+ if (mHasEndImage) {
+ throw new IllegalArgumentException("Trying to add an action to end items when an"
+ + "icon has already been added. End items cannot have a mixture of "
+ + "tappable icons and icons.");
+ }
+ Slice actionSlice = new Slice.Builder(getBuilder()).addIcon(icon, null).build();
+ mEndItems.add(new SliceItem(action, actionSlice, FORMAT_ACTION, null, new String[0]));
+ mHasEndAction = true;
+ return this;
+ }
+
+ /**
+ * Adds a toggle action to the template. If there is a toggle to display, any end items
+ * that were added will not be shown. Only one toggle can be added to a row, this will
+ * throw {@link IllegalArgumentException} if one has already been added.
+ */
+ @NonNull
+ public RowBuilder addToggle(@NonNull PendingIntent action, boolean isChecked) {
+ if (mHasToggle) {
+ throw new IllegalArgumentException("Trying to add a toggle when one has already "
+ + "been added.");
+ }
+ @Slice.SliceHint String[] hints = isChecked
+ ? new String[] {SUBTYPE_TOGGLE, HINT_SELECTED}
+ : new String[] {SUBTYPE_TOGGLE};
+ Slice s = new Slice.Builder(getBuilder()).addHints(hints).build();
+ mEndItems.add(0, new SliceItem(action, s, FORMAT_ACTION, null, hints));
+ mHasToggle = true;
+ return this;
+ }
+
+ /**
+ * Adds a toggle action to the template with custom icons to represent checked and unchecked
+ * state. If there is a toggle to display, any end items that were added will not be shown.
+ * Only one toggle can be added to a row, this will throw {@link IllegalArgumentException}
+ * if one has already been added.
+ */
+ @NonNull
+ public RowBuilder addToggle(@NonNull PendingIntent action, boolean isChecked,
+ @NonNull Icon icon) {
+ if (mHasToggle) {
+ throw new IllegalArgumentException("Trying to add a toggle when one has already "
+ + "been added.");
+ }
+ @Slice.SliceHint String[] hints = isChecked
+ ? new String[] {SliceHints.SUBTYPE_TOGGLE, HINT_SELECTED}
+ : new String[] {SliceHints.SUBTYPE_TOGGLE};
+ Slice actionSlice = new Slice.Builder(getBuilder())
+ .addIcon(icon, null)
+ .addHints(hints).build();
+ mEndItems.add(0, new SliceItem(action, actionSlice, FORMAT_ACTION, null, hints));
+ mHasToggle = true;
+ return this;
+ }
+
+ @Override
+ public void apply(Slice.Builder b) {
+ Slice.Builder wrapped = b;
+ if (mContentIntent != null) {
+ b = new Slice.Builder(wrapped);
+ }
+ if (mStartItem != null) {
+ b.addItem(mStartItem);
+ }
+ if (mTitleItem != null) {
+ b.addItem(mTitleItem);
+ }
+ if (mSubtitleItem != null) {
+ b.addItem(mSubtitleItem);
+ }
+ for (int i = 0; i < mEndItems.size(); i++) {
+ SliceItem item = mEndItems.get(i);
+ b.addItem(item);
+ }
+ if (mContentIntent != null) {
+ wrapped.addAction(mContentIntent, b.build(), null);
+ }
+ wrapped.addHints(mIsHeader ? null : HINT_LIST_ITEM);
+ }
+ }
+}
diff --git a/androidx/app/slice/builders/MessagingSliceBuilder.java b/androidx/app/slice/builders/MessagingSliceBuilder.java
index 424c8efb..66bb3457 100644
--- a/androidx/app/slice/builders/MessagingSliceBuilder.java
+++ b/androidx/app/slice/builders/MessagingSliceBuilder.java
@@ -20,9 +20,13 @@ import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.graphics.drawable.Icon;
import android.net.Uri;
+import android.os.Build;
import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
+import java.util.function.Consumer;
+
import androidx.app.slice.Slice;
/**
@@ -41,38 +45,42 @@ public class MessagingSliceBuilder extends TemplateSliceBuilder {
}
/**
- * Create a {@link MessageBuilder} that will be added to this slice when
- * {@link MessageBuilder#endMessage()} is called.
- * @return a new message builder
+ * @hide
*/
- public MessageBuilder startMessage() {
- return new MessageBuilder(this);
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void apply(androidx.app.slice.Slice.Builder builder) {
+
}
/**
- * @hide
+ * Add a subslice to this builder.
*/
- @RestrictTo(LIBRARY_GROUP)
- @Override
- public void apply(Slice.Builder builder) {
+ public MessagingSliceBuilder add(MessageBuilder builder) {
+ getBuilder().addSubSlice(builder.build());
+ return this;
}
- @Override
- public void add(SubTemplateSliceBuilder builder) {
- getBuilder().addSubSlice(builder.build(), android.app.slice.Slice.SUBTYPE_MESSAGE);
+ /**
+ * Add a subslice to this builder.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public MessagingSliceBuilder add(Consumer<MessageBuilder> c) {
+ MessageBuilder b = new MessageBuilder(this);
+ c.accept(b);
+ return add(b);
}
/**
* Builder for adding a message to {@link MessagingSliceBuilder}.
*/
- public static final class MessageBuilder
- extends SubTemplateSliceBuilder<MessagingSliceBuilder> {
+ public static final class MessageBuilder extends TemplateSliceBuilder {
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public MessageBuilder(MessagingSliceBuilder parent) {
- super(parent.createChildBuilder(), parent);
+ super(parent.createChildBuilder());
}
/**
@@ -99,12 +107,8 @@ public class MessagingSliceBuilder extends TemplateSliceBuilder {
return this;
}
- /**
- * Complete the construction of this message and add it to the parent builder.
- * @return the parent builder so construction can continue.
- */
- public MessagingSliceBuilder endMessage() {
- return finish();
+ @Override
+ public void apply(Slice.Builder builder) {
}
}
}
diff --git a/androidx/app/slice/builders/TemplateSliceBuilder.java b/androidx/app/slice/builders/TemplateSliceBuilder.java
index 102ee001..464f940a 100644
--- a/androidx/app/slice/builders/TemplateSliceBuilder.java
+++ b/androidx/app/slice/builders/TemplateSliceBuilder.java
@@ -16,6 +16,7 @@
package androidx.app.slice.builders;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import android.net.Uri;
import android.support.annotation.RestrictTo;
@@ -36,7 +37,15 @@ public abstract class TemplateSliceBuilder {
/**
* @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @RestrictTo(LIBRARY)
+ protected TemplateSliceBuilder(Slice.Builder b) {
+ mSliceBuilder = b;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
public Slice.Builder getBuilder() {
return mSliceBuilder;
}
@@ -44,7 +53,7 @@ public abstract class TemplateSliceBuilder {
/**
* @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @RestrictTo(LIBRARY)
public Slice.Builder createChildBuilder() {
return new Slice.Builder(mSliceBuilder);
}
@@ -60,69 +69,6 @@ public abstract class TemplateSliceBuilder {
/**
* @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @RestrictTo(LIBRARY)
public abstract void apply(Slice.Builder builder);
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public abstract void add(SubTemplateSliceBuilder builder);
-
- /**
- * Base class of builders for sub-slices of {@link TemplateSliceBuilder}s.
- * @param <T> Type of parent
- */
- public abstract static class SubTemplateSliceBuilder<T extends TemplateSliceBuilder> {
-
- private final Slice.Builder mBuilder;
- private final T mParent;
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public SubTemplateSliceBuilder(Slice.Builder builder, T parent) {
- mBuilder = builder;
- mParent = parent;
- }
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public SubTemplateSliceBuilder(Slice.Builder builder) {
- mBuilder = builder;
- mParent = null;
- }
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public Slice.Builder getBuilder() {
- return mBuilder;
- }
-
- /**
- * Construct the slice.
- */
- public Slice build() {
- return mBuilder.build();
- }
-
- /**
- * Construct the slice and return to the parent object. If this object was not
- * created from a {@link TemplateSliceBuilder} it will return null.
- * @return parent builder
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public T finish() {
- if (mParent != null) {
- mParent.add(this);
- }
- return mParent;
- }
- }
}
diff --git a/androidx/app/slice/compat/ContentProviderWrapper.java b/androidx/app/slice/compat/ContentProviderWrapper.java
index 944a5ac0..9e02b3ae 100644
--- a/androidx/app/slice/compat/ContentProviderWrapper.java
+++ b/androidx/app/slice/compat/ContentProviderWrapper.java
@@ -43,8 +43,8 @@ public class ContentProviderWrapper extends ContentProvider {
*/
public void attachInfo(Context context, ProviderInfo info, ContentProvider impl) {
mImpl = impl;
- mImpl.attachInfo(context, info);
super.attachInfo(context, info);
+ mImpl.attachInfo(context, info);
}
@Override
diff --git a/androidx/app/slice/compat/SliceProviderCompat.java b/androidx/app/slice/compat/SliceProviderCompat.java
index 9fcac1b5..503ba0a5 100644
--- a/androidx/app/slice/compat/SliceProviderCompat.java
+++ b/androidx/app/slice/compat/SliceProviderCompat.java
@@ -41,11 +41,13 @@ import android.support.annotation.RestrictTo;
import android.support.annotation.RestrictTo.Scope;
import android.util.Log;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import androidx.app.slice.Slice;
import androidx.app.slice.SliceProvider;
+import androidx.app.slice.SliceSpec;
/**
* @hide
@@ -60,6 +62,8 @@ public class SliceProviderCompat extends ContentProvider {
public static final String METHOD_MAP_INTENT = "map_slice";
public static final String EXTRA_INTENT = "slice_intent";
public static final String EXTRA_SLICE = "slice";
+ public static final String EXTRA_SUPPORTED_SPECS = "specs";
+ public static final String EXTRA_SUPPORTED_SPECS_REVS = "revs";
private static final boolean DEBUG = false;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -130,8 +134,9 @@ public class SliceProviderCompat extends ContentProvider {
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
"Slice binding requires the permission BIND_SLICE");
}
+ List<SliceSpec> specs = getSpecs(extras);
- Slice s = handleBindSlice(uri);
+ Slice s = handleBindSlice(uri, specs);
Bundle b = new Bundle();
b.putParcelable(EXTRA_SLICE, s.toBundle());
return b;
@@ -144,7 +149,8 @@ public class SliceProviderCompat extends ContentProvider {
Uri uri = mSliceProvider.onMapIntentToUri(intent);
Bundle b = new Bundle();
if (uri != null) {
- Slice s = handleBindSlice(uri);
+ List<SliceSpec> specs = getSpecs(extras);
+ Slice s = handleBindSlice(uri, specs);
b.putParcelable(EXTRA_SLICE, s.toBundle());
} else {
b.putParcelable(EXTRA_SLICE, null);
@@ -154,16 +160,16 @@ public class SliceProviderCompat extends ContentProvider {
return super.call(method, arg, extras);
}
- private Slice handleBindSlice(final Uri sliceUri) {
+ private Slice handleBindSlice(final Uri sliceUri, final List<SliceSpec> specs) {
if (Looper.myLooper() == Looper.getMainLooper()) {
- return onBindSliceStrict(sliceUri);
+ return onBindSliceStrict(sliceUri, specs);
} else {
final CountDownLatch latch = new CountDownLatch(1);
final Slice[] output = new Slice[1];
mHandler.post(new Runnable() {
@Override
public void run() {
- output[0] = onBindSliceStrict(sliceUri);
+ output[0] = onBindSliceStrict(sliceUri, specs);
latch.countDown();
}
});
@@ -176,13 +182,14 @@ public class SliceProviderCompat extends ContentProvider {
}
}
- private Slice onBindSliceStrict(Uri sliceUri) {
+ private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> specs) {
ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
try {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build());
+ SliceProvider.setSpecs(specs);
return mSliceProvider.onBindSlice(sliceUri);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
@@ -190,9 +197,10 @@ public class SliceProviderCompat extends ContentProvider {
}
/**
- * Compat version of {@link Slice#bindSlice(Context, Uri)}.
+ * Compat version of {@link Slice#bindSlice}.
*/
- public static Slice bindSlice(Context context, Uri uri) {
+ public static Slice bindSlice(Context context, Uri uri,
+ List<SliceSpec> supportedSpecs) {
ContentProviderClient provider = context.getContentResolver()
.acquireContentProviderClient(uri);
if (provider == null) {
@@ -201,6 +209,7 @@ public class SliceProviderCompat extends ContentProvider {
try {
Bundle extras = new Bundle();
extras.putParcelable(EXTRA_BIND_URI, uri);
+ addSpecs(extras, supportedSpecs);
final Bundle res = provider.call(METHOD_SLICE, null, extras);
if (res == null) {
return null;
@@ -219,16 +228,38 @@ public class SliceProviderCompat extends ContentProvider {
}
}
+ private static void addSpecs(Bundle extras, List<SliceSpec> supportedSpecs) {
+ ArrayList<String> types = new ArrayList<>();
+ ArrayList<Integer> revs = new ArrayList<>();
+ for (SliceSpec spec : supportedSpecs) {
+ types.add(spec.getType());
+ revs.add(spec.getRevision());
+ }
+ extras.putStringArrayList(EXTRA_SUPPORTED_SPECS, types);
+ extras.putIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS, revs);
+ }
+
+ private static List<SliceSpec> getSpecs(Bundle extras) {
+ ArrayList<SliceSpec> specs = new ArrayList<>();
+ ArrayList<String> types = extras.getStringArrayList(EXTRA_SUPPORTED_SPECS);
+ ArrayList<Integer> revs = extras.getIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS);
+ for (int i = 0; i < types.size(); i++) {
+ specs.add(new SliceSpec(types.get(i), revs.get(i)));
+ }
+ return specs;
+ }
+
/**
- * Compat version of {@link Slice#bindSlice(Context, Intent)}.
+ * Compat version of {@link Slice#bindSlice}.
*/
- public static Slice bindSlice(Context context, Intent intent) {
+ public static Slice bindSlice(Context context, Intent intent,
+ List<SliceSpec> supportedSpecs) {
ContentResolver resolver = context.getContentResolver();
// Check if the intent has data for the slice uri on it and use that
final Uri intentData = intent.getData();
if (intentData != null && SLICE_TYPE.equals(resolver.getType(intentData))) {
- return bindSlice(context, intentData);
+ return bindSlice(context, intentData, supportedSpecs);
}
// Otherwise ask the app
List<ResolveInfo> providers =
@@ -246,6 +277,7 @@ public class SliceProviderCompat extends ContentProvider {
try {
Bundle extras = new Bundle();
extras.putParcelable(EXTRA_INTENT, intent);
+ addSpecs(extras, supportedSpecs);
final Bundle res = provider.call(METHOD_MAP_INTENT, null, extras);
if (res == null) {
return null;
diff --git a/androidx/app/slice/compat/SliceProviderWrapper.java b/androidx/app/slice/compat/SliceProviderWrapper.java
index 3afed2b0..438b9641 100644
--- a/androidx/app/slice/compat/SliceProviderWrapper.java
+++ b/androidx/app/slice/compat/SliceProviderWrapper.java
@@ -15,6 +15,8 @@
*/
package androidx.app.slice.compat;
+import static androidx.app.slice.SliceConvert.wrap;
+
import android.annotation.TargetApi;
import android.app.slice.Slice;
import android.app.slice.SliceProvider;
@@ -49,6 +51,7 @@ public class SliceProviderWrapper extends SliceProvider {
@Override
public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedVersions) {
+ androidx.app.slice.SliceProvider.setSpecs(wrap(supportedVersions));
return SliceConvert.unwrap(mSliceProvider.onBindSlice(sliceUri));
}
diff --git a/androidx/app/slice/core/SliceHints.java b/androidx/app/slice/core/SliceHints.java
index 6ebcd03a..34acf934 100644
--- a/androidx/app/slice/core/SliceHints.java
+++ b/androidx/app/slice/core/SliceHints.java
@@ -28,23 +28,21 @@ import android.support.annotation.RestrictTo;
@RestrictTo(LIBRARY_GROUP)
public class SliceHints {
/**
- * Hint to indicate that this content has a toggle action associated with it. To indicate that
- * the toggle is on, use {@link Slice#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.
+ * Subtype to indicate that this content has a toggle action associated with it. To indicate
+ * that the toggle is on, use {@link Slice#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 HINT_TOGGLE = "toggle";
+ public static final String SUBTYPE_TOGGLE = "toggle";
/**
* Key to retrieve an extra added to an intent when a control is changed.
*/
public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
+
/**
- * Hint to indicate that this content should not be shown in the
- * {@link androidx.app.slice.widget.SliceView#MODE_SMALL}
- * and {@link androidx.app.slice.widget.SliceView#MODE_LARGE} modes of SliceView.
- * This content may be used to populate
- * the {@link androidx.app.slice.widget.SliceView#MODE_SHORTCUT} format of the slice.
+ * 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";
}
diff --git a/androidx/app/slice/core/SliceQuery.java b/androidx/app/slice/core/SliceQuery.java
index 9da5478f..8ab6c9a6 100644
--- a/androidx/app/slice/core/SliceQuery.java
+++ b/androidx/app/slice/core/SliceQuery.java
@@ -16,12 +16,9 @@
package androidx.app.slice.core;
-import static android.app.slice.Slice.HINT_ACTIONS;
-import static android.app.slice.Slice.HINT_LIST;
-import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.SliceItem.FORMAT_ACTION;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
@@ -29,8 +26,8 @@ import android.annotation.TargetApi;
import android.support.annotation.RestrictTo;
import android.text.TextUtils;
+import java.util.ArrayDeque;
import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Spliterators;
@@ -53,11 +50,11 @@ public class SliceQuery {
/**
* @return Whether this item is appropriate to be considered a "start" item, i.e. go in the
- * front slot of a small slice.
+ * front slot of a row.
*/
public static boolean isStartType(SliceItem item) {
final String type = item.getFormat();
- return (!item.hasHint(SliceHints.HINT_TOGGLE)
+ return (!item.hasHint(SliceHints.SUBTYPE_TOGGLE)
&& (FORMAT_ACTION.equals(type) && (find(item, FORMAT_IMAGE) != null)))
|| FORMAT_IMAGE.equals(type)
|| FORMAT_TIMESTAMP.equals(type);
@@ -95,12 +92,13 @@ public class SliceQuery {
SliceItem child = items.get(i);
if (FORMAT_IMAGE.equals(child.getFormat()) && !hasImage) {
hasImage = true;
- } else if (FORMAT_COLOR.equals(child.getFormat())) {
+ } else if (FORMAT_INT.equals(child.getFormat())) {
continue;
} else {
return false;
}
}
+ return hasImage;
}
return false;
}
@@ -144,25 +142,6 @@ public class SliceQuery {
return true;
}
- /**
- */
- public static SliceItem getPrimaryIcon(Slice slice) {
- for (SliceItem item : slice.getItems()) {
- if (FORMAT_IMAGE.equals(item.getFormat())) {
- return item;
- }
- if (!(FORMAT_SLICE.equals(item.getFormat()) && item.hasHint(HINT_LIST))
- && !item.hasHint(HINT_ACTIONS)
- && !item.hasHint(HINT_LIST_ITEM)
- && !FORMAT_ACTION.equals(item.getFormat())) {
- SliceItem icon = SliceQuery.find(item, FORMAT_IMAGE);
- if (icon != null) {
- return icon;
- }
- }
- }
- return null;
- }
/**
*/
@@ -273,6 +252,17 @@ public class SliceQuery {
/**
*/
+ public static SliceItem findSubtype(Slice s, final String format, final String subtype) {
+ return stream(s).filter(new Predicate<SliceItem>() {
+ @Override
+ public boolean test(SliceItem item) {
+ return checkFormat(item, format) && checkSubtype(item, subtype);
+ }
+ }).findFirst().orElse(null);
+ }
+
+ /**
+ */
public static SliceItem findSubtype(SliceItem s, final String format, final String subtype) {
return stream(s).filter(new Predicate<SliceItem>() {
@Override
@@ -282,8 +272,8 @@ public class SliceQuery {
}).findFirst().orElse(null);
}
- /**
- */
+ /**
+ */
public static SliceItem find(SliceItem s, final String format, final String[] hints,
final String[] nonHints) {
return stream(s).filter(new Predicate<SliceItem>() {
@@ -306,7 +296,7 @@ public class SliceQuery {
/**
*/
public static Stream<SliceItem> stream(SliceItem slice) {
- Queue<SliceItem> items = new LinkedList();
+ Queue<SliceItem> items = new ArrayDeque<>();
items.add(slice);
return getSliceItemStream(items);
}
@@ -314,7 +304,7 @@ public class SliceQuery {
/**
*/
public static Stream<SliceItem> stream(Slice slice) {
- Queue<SliceItem> items = new LinkedList();
+ Queue<SliceItem> items = new ArrayDeque<>();
items.addAll(slice.getItems());
return getSliceItemStream(items);
}
diff --git a/androidx/app/slice/widget/ActionRow.java b/androidx/app/slice/widget/ActionRow.java
index 70cec4fe..59dd3c77 100644
--- a/androidx/app/slice/widget/ActionRow.java
+++ b/androidx/app/slice/widget/ActionRow.java
@@ -17,9 +17,10 @@
package androidx.app.slice.widget;
import static android.app.slice.Slice.HINT_NO_TINT;
+import static android.app.slice.Slice.SUBTYPE_COLOR;
import static android.app.slice.SliceItem.FORMAT_ACTION;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import android.annotation.TargetApi;
@@ -109,12 +110,12 @@ public class ActionRow extends FrameLayout {
mActionsGroup.removeAllViews();
addView(mActionsGroup);
- SliceItem color = SliceQuery.find(actionRow, FORMAT_COLOR);
+ SliceItem color = SliceQuery.findSubtype(actionRow, FORMAT_INT, SUBTYPE_COLOR);
if (color == null) {
color = defColor;
}
if (color != null) {
- setColor(color.getColor());
+ setColor(color.getInt());
}
SliceQuery.findAll(actionRow, FORMAT_ACTION).forEach(new Consumer<SliceItem>() {
@Override
diff --git a/androidx/app/slice/widget/GridRowView.java b/androidx/app/slice/widget/GridRowView.java
new file mode 100644
index 00000000..6a5f44b3
--- /dev/null
+++ b/androidx/app/slice/widget/GridRowView.java
@@ -0,0 +1,354 @@
+/*
+ * 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 androidx.app.slice.widget;
+
+import static android.app.slice.Slice.HINT_LARGE;
+import static android.app.slice.Slice.HINT_NO_TINT;
+import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.SliceItem.FORMAT_ACTION;
+import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_SLICE;
+import static android.app.slice.SliceItem.FORMAT_TEXT;
+import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.os.AsyncTask;
+import android.support.annotation.RestrictTo;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceItem;
+import androidx.app.slice.core.SliceQuery;
+import androidx.app.slice.view.R;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@TargetApi(24)
+public class GridRowView extends LinearLayout implements LargeSliceAdapter.SliceListView,
+ View.OnClickListener, SliceView.SliceModeView {
+
+ private static final String TAG = "GridView";
+
+ // TODO -- Should addRow notion to the builder so that apps could define the "see more" intent
+ private static final boolean ALLOW_SEE_MORE = false;
+
+ private static final int TITLE_TEXT_LAYOUT = R.layout.abc_slice_title;
+ private static final int TEXT_LAYOUT = R.layout.abc_slice_secondary_text;
+
+ // Max number of *just* images that can be shown in a row
+ private static final int MAX_IMAGES = 3;
+ // Max number of normal cell items that can be shown in a row
+ private static final int MAX_ALL = 5;
+
+ // Max number of text items that can show in a cell
+ private static final int MAX_CELL_TEXT = 2;
+ // Max number of text items that can show in a cell if the mode is small
+ private static final int MAX_CELL_TEXT_SMALL = 1;
+ // Max number of images that can show in a cell
+ private static final int MAX_CELL_IMAGES = 1;
+
+ private SliceItem mColorItem;
+ private boolean mIsAllImages;
+ private @SliceView.SliceMode int mSliceMode = 0;
+
+ private int mIconSize;
+ private int mLargeIconSize;
+ private int mBigPictureHeight;
+ private int mAllImagesHeight;
+
+ public GridRowView(Context context) {
+ this(context, null);
+ }
+
+ public GridRowView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ final Resources res = getContext().getResources();
+ mIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_icon_size);
+ mLargeIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_large_icon_size);
+ mBigPictureHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_big_picture_height);
+ mAllImagesHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_image_only_height);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mIsAllImages) {
+ int count = getChildCount();
+ int height = (count == 1) ? mBigPictureHeight : mAllImagesHeight;
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ getLayoutParams().height = height;
+ for (int i = 0; i < count; i++) {
+ getChildAt(i).getLayoutParams().height = height;
+ }
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /**
+ * Set the color for the items in this view.
+ */
+ @Override
+ public void setColor(SliceItem colorItem) {
+ mColorItem = colorItem;
+ }
+
+ @Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
+ public int getMode() {
+ return mSliceMode;
+ }
+
+ /**
+ * This is called when GridView is being used as a small template.
+ */
+ @Override
+ public void setSlice(Slice slice) {
+ mSliceMode = SliceView.MODE_SMALL;
+ Slice.Builder sb = new Slice.Builder(slice.getUri());
+ sb.addSubSlice(slice);
+ Slice parentSlice = sb.build();
+ populateViews(parentSlice.getItems().get(0));
+ }
+
+ /**
+ * This is called when GridView is being used as a component in a large template.
+ */
+ @Override
+ public void setSliceItem(SliceItem slice, boolean isHeader) {
+ mSliceMode = SliceView.MODE_LARGE;
+ populateViews(slice);
+ }
+
+ private void populateViews(SliceItem slice) {
+ mIsAllImages = true;
+ removeAllViews();
+ int total = 1;
+ if (FORMAT_SLICE.equals(slice.getFormat())) {
+ List<SliceItem> items = slice.getSlice().getItems();
+ // Check if it it's only one item that is a slice
+ if (items.size() == 1 && items.get(0).getFormat().equals(FORMAT_SLICE)) {
+ items = items.get(0).getSlice().getItems();
+ }
+ total = items.size();
+ for (int i = 0; i < total; i++) {
+ SliceItem item = items.get(i);
+ if (isFull()) {
+ continue;
+ }
+ if (!addCell(item)) {
+ mIsAllImages = false;
+ }
+ }
+ } else if (!isFull()) {
+ if (!addCell(slice)) {
+ mIsAllImages = false;
+ }
+ }
+ if (ALLOW_SEE_MORE && mIsAllImages && total > getChildCount()) {
+ addSeeMoreCount(total - getChildCount());
+ }
+ }
+
+ private void addSeeMoreCount(int numExtra) {
+ View last = getChildAt(getChildCount() - 1);
+ FrameLayout frame = new FrameLayout(getContext());
+ frame.setLayoutParams(last.getLayoutParams());
+
+ removeView(last);
+ frame.addView(last, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+
+ TextView v = new TextView(getContext());
+ v.setTextColor(Color.WHITE);
+ v.setBackgroundColor(0x4d000000);
+ v.setText(getResources().getString(R.string.abc_slice_more_content, numExtra));
+ v.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
+ v.setGravity(Gravity.CENTER);
+ frame.addView(v, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
+
+ addView(frame);
+ }
+
+ private boolean isFull() {
+ return getChildCount() >= (mIsAllImages ? MAX_IMAGES : MAX_ALL);
+ }
+
+ /**
+ * Adds a cell to the grid view based on the provided {@link SliceItem}.
+ * @return true if this item is just an image.
+ */
+ private boolean addCell(SliceItem sliceItem) {
+ final int maxCellText = mSliceMode == SliceView.MODE_SMALL
+ ? MAX_CELL_TEXT_SMALL
+ : MAX_CELL_TEXT;
+ LinearLayout cellContainer = new LinearLayout(getContext());
+ cellContainer.setOrientation(LinearLayout.VERTICAL);
+ cellContainer.setGravity(Gravity.CENTER_HORIZONTAL);
+ final int color = mColorItem != null ? mColorItem.getInt() : -1;
+ final String format = sliceItem.getFormat();
+ if (FORMAT_SLICE.equals(format) || FORMAT_ACTION.equals(format)) {
+ // It's a slice -- try to add all the items we can to a cell.
+ List<SliceItem> items = sliceItem.getSlice().getItems();
+ SliceItem actionItem = null;
+ if (FORMAT_ACTION.equals(format)) {
+ actionItem = sliceItem;
+ }
+ if (items.size() == 1 && FORMAT_ACTION.equals(items.get(0).getFormat())) {
+ actionItem = items.get(0);
+ items = items.get(0).getSlice().getItems();
+ }
+ boolean imagesOnly = true;
+ int textCount = 0;
+ int imageCount = 0;
+ boolean added = false;
+ boolean singleItem = items.size() == 1;
+ List<SliceItem> textItems = null;
+ // In small format we display one text item and prefer titles
+ if (!singleItem && mSliceMode == SliceView.MODE_SMALL) {
+ // Get all our text items
+ textItems = items.stream().filter(new Predicate<SliceItem>() {
+ @Override
+ public boolean test(SliceItem s) {
+ return FORMAT_TEXT.equals(s.getFormat());
+ }
+ }).collect(Collectors.<SliceItem>toList());
+ // If we have more than 1 remove non-titles
+ Iterator<SliceItem> iterator = textItems.iterator();
+ while (textItems.size() > 1) {
+ SliceItem item = iterator.next();
+ if (!item.hasHint(HINT_TITLE)) {
+ iterator.remove();
+ }
+ }
+ }
+ for (int i = 0; i < items.size(); i++) {
+ SliceItem item = items.get(i);
+ final String itemFormat = item.getFormat();
+ if (textCount < maxCellText && (FORMAT_TEXT.equals(itemFormat)
+ || FORMAT_TIMESTAMP.equals(itemFormat))) {
+ if (textItems != null && !textItems.contains(item)) {
+ continue;
+ }
+ if (addItem(item, color, cellContainer, singleItem)) {
+ textCount++;
+ imagesOnly = false;
+ added = true;
+ }
+ } else if (imageCount < MAX_CELL_IMAGES && FORMAT_IMAGE.equals(item.getFormat())) {
+ if (addItem(item, color, cellContainer, singleItem)) {
+ imageCount++;
+ added = true;
+ }
+ }
+ }
+ if (added) {
+ addView(cellContainer, new LayoutParams(0, WRAP_CONTENT, 1));
+ if (actionItem != null) {
+ cellContainer.setTag(actionItem);
+ makeClickable(cellContainer);
+ }
+ }
+ return imagesOnly;
+ } else if (addItem(sliceItem, color, this, true)) {
+ return FORMAT_IMAGE.equals(sliceItem.getFormat());
+ }
+ return false;
+ }
+
+ /**
+ * Adds simple items to a container. Simple items include icons, text, and timestamps.
+ * @return Whether an item was added.
+ */
+ private boolean addItem(SliceItem item, int color, ViewGroup container, boolean singleItem) {
+ final String format = item.getFormat();
+ View addedView = null;
+ if (FORMAT_TEXT.equals(format) || FORMAT_TIMESTAMP.equals(format)) {
+ boolean title = SliceQuery.hasAnyHints(item, HINT_LARGE, HINT_TITLE);
+ TextView tv = (TextView) LayoutInflater.from(getContext()).inflate(title
+ ? TITLE_TEXT_LAYOUT : TEXT_LAYOUT, null);
+ CharSequence text = FORMAT_TIMESTAMP.equals(format)
+ ? SliceViewUtil.getRelativeTimeString(item.getTimestamp())
+ : item.getText();
+ tv.setText(text);
+ container.addView(tv);
+ addedView = tv;
+ } else if (FORMAT_IMAGE.equals(format)) {
+ ImageView iv = new ImageView(getContext());
+ iv.setImageIcon(item.getIcon());
+ if (color != -1 && !item.hasHint(HINT_NO_TINT)) {
+ iv.setColorFilter(color);
+ }
+ int size = mIconSize;
+ if (item.hasHint(HINT_LARGE)) {
+ iv.setScaleType(ScaleType.CENTER_CROP);
+ size = singleItem ? MATCH_PARENT : mLargeIconSize;
+ }
+ container.addView(iv, new LayoutParams(size, size));
+ addedView = iv;
+ }
+ return addedView != null;
+ }
+
+ private void makeClickable(View layout) {
+ layout.setOnClickListener(this);
+ layout.setBackground(SliceViewUtil.getDrawable(getContext(),
+ android.R.attr.selectableItemBackground));
+ }
+
+ @Override
+ public void onClick(View view) {
+ final SliceItem actionTag = (SliceItem) view.getTag();
+ if (actionTag != null && FORMAT_ACTION.equals(actionTag.getFormat())) {
+ AsyncTask.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ actionTag.getAction().send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "PendingIntent for slice cannot be sent", e);
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/androidx/app/slice/widget/GridView.java b/androidx/app/slice/widget/GridView.java
deleted file mode 100644
index 2aa57bd4..00000000
--- a/androidx/app/slice/widget/GridView.java
+++ /dev/null
@@ -1,203 +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 androidx.app.slice.widget;
-
-import static android.app.slice.Slice.HINT_LARGE;
-import static android.app.slice.Slice.HINT_TITLE;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
-import static android.app.slice.SliceItem.FORMAT_IMAGE;
-import static android.app.slice.SliceItem.FORMAT_SLICE;
-import static android.app.slice.SliceItem.FORMAT_TEXT;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.graphics.Color;
-import android.support.annotation.RestrictTo;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.ImageView.ScaleType;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Consumer;
-
-import androidx.app.slice.SliceItem;
-import androidx.app.slice.core.SliceQuery;
-import androidx.app.slice.view.R;
-
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-@TargetApi(24)
-public class GridView extends LinearLayout implements LargeSliceAdapter.SliceListView {
-
- private static final String TAG = "GridView";
-
- private static final int MAX_IMAGES = 3;
- private static final int MAX_ALL = 5;
- private boolean mIsAllImages;
-
- public GridView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (mIsAllImages) {
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = width / getChildCount();
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
- getLayoutParams().height = height;
- for (int i = 0; i < getChildCount(); i++) {
- getChildAt(i).getLayoutParams().height = height;
- }
- }
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- public void setSliceItem(SliceItem slice) {
- mIsAllImages = true;
- removeAllViews();
- int total = 1;
- if (FORMAT_SLICE.equals(slice.getFormat())) {
- List<SliceItem> items = slice.getSlice().getItems();
- total = items.size();
- for (int i = 0; i < total; i++) {
- SliceItem item = items.get(i);
- if (isFull()) {
- continue;
- }
- if (!addItem(item)) {
- mIsAllImages = false;
- }
- }
- } else {
- if (!isFull()) {
- if (!addItem(slice)) {
- mIsAllImages = false;
- }
- }
- }
- if (total > getChildCount() && mIsAllImages) {
- addExtraCount(total - getChildCount());
- }
- }
-
- @Override
- public void setColor(SliceItem color) {
-
- }
-
- private void addExtraCount(int numExtra) {
- View last = getChildAt(getChildCount() - 1);
- FrameLayout frame = new FrameLayout(getContext());
- frame.setLayoutParams(last.getLayoutParams());
-
- removeView(last);
- frame.addView(last, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-
- TextView v = new TextView(getContext());
- v.setTextColor(Color.WHITE);
- v.setBackgroundColor(0x4d000000);
- v.setText(getResources().getString(R.string.abc_slice_more_content, numExtra));
- v.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
- v.setGravity(Gravity.CENTER);
- frame.addView(v, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
-
- addView(frame);
- }
-
- private boolean isFull() {
- return getChildCount() >= (mIsAllImages ? MAX_IMAGES : MAX_ALL);
- }
-
- /**
- * Returns true if this item is just an image.
- */
- private boolean addItem(final SliceItem item) {
- if (FORMAT_IMAGE.equals(item.getFormat())) {
- ImageView v = new ImageView(getContext());
- v.setImageIcon(item.getIcon());
- v.setScaleType(ScaleType.CENTER_CROP);
- addView(v, new LayoutParams(0, MATCH_PARENT, 1));
- return true;
- } else {
- final LinearLayout v = new LinearLayout(getContext());
- int s = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
- 12, getContext().getResources().getDisplayMetrics());
- v.setPadding(0, s, 0, 0);
- v.setOrientation(LinearLayout.VERTICAL);
- v.setGravity(Gravity.CENTER_HORIZONTAL);
- // TODO: Unify sporadic inflates that happen throughout the code.
- ArrayList<SliceItem> items = new ArrayList<>();
- if (FORMAT_SLICE.equals(item.getFormat())) {
- items.addAll(item.getSlice().getItems());
- }
- items.forEach(new Consumer<SliceItem>() {
- @Override
- public void accept(SliceItem i) {
- Context context = getContext();
- switch (i.getFormat()) {
- case FORMAT_TEXT:
- boolean title = false;
- if ((SliceQuery.hasAnyHints(item, new String[]{
- HINT_LARGE, HINT_TITLE
- }))) {
- title = true;
- }
- TextView tv = (TextView) LayoutInflater.from(context).inflate(title
- ? R.layout.abc_slice_title
- : R.layout.abc_slice_secondary_text,
- null);
- tv.setText(i.getText());
- v.addView(tv);
- break;
- case FORMAT_IMAGE:
- ImageView iv = new ImageView(context);
- iv.setImageIcon(i.getIcon());
- if (item.hasHint(HINT_LARGE)) {
- iv.setLayoutParams(new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
- } else {
- int size = (int) TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- 48, context.getResources().getDisplayMetrics());
- iv.setLayoutParams(new LayoutParams(size, size));
- }
- v.addView(iv);
- break;
- case FORMAT_COLOR:
- // TODO: Support color to tint stuff here.
- break;
- }
- }
- });
- addView(v, new LayoutParams(0, WRAP_CONTENT, 1));
- return false;
- }
- }
-}
diff --git a/androidx/app/slice/widget/LargeSliceAdapter.java b/androidx/app/slice/widget/LargeSliceAdapter.java
index 335eb23f..b8f4bcf5 100644
--- a/androidx/app/slice/widget/LargeSliceAdapter.java
+++ b/androidx/app/slice/widget/LargeSliceAdapter.java
@@ -19,11 +19,12 @@ package androidx.app.slice.widget;
import static android.app.slice.Slice.HINT_HORIZONTAL;
import static android.app.slice.Slice.SUBTYPE_MESSAGE;
import static android.app.slice.Slice.SUBTYPE_SOURCE;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import android.annotation.TargetApi;
+import android.app.slice.Slice;
import android.content.Context;
import android.support.annotation.RestrictTo;
import android.support.v7.widget.RecyclerView;
@@ -51,7 +52,7 @@ import androidx.app.slice.view.R;
public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.SliceViewHolder> {
public static final int TYPE_DEFAULT = 1;
- public static final int TYPE_HEADER = 2;
+ public static final int TYPE_HEADER = 2; // TODO headers shouldn't scroll off
public static final int TYPE_GRID = 3;
public static final int TYPE_MESSAGE = 4;
public static final int TYPE_MESSAGE_LOCAL = 5;
@@ -108,7 +109,7 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
SliceWrapper slice = mSlices.get(position);
if (holder.mSliceView != null) {
holder.mSliceView.setColor(mColor);
- holder.mSliceView.setSliceItem(slice.mItem);
+ holder.mSliceView.setSliceItem(slice.mItem, position == 0 /* isHeader */);
}
}
@@ -122,7 +123,7 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
return LayoutInflater.from(mContext).inflate(R.layout.abc_slice_message_local,
null);
}
- return new SmallTemplateView(mContext);
+ return new RowView(mContext);
}
protected static class SliceWrapper {
@@ -148,6 +149,9 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
if (item.hasHint(HINT_HORIZONTAL)) {
return TYPE_GRID;
}
+ if (!item.hasHint(Slice.HINT_LIST_ITEM)) {
+ return TYPE_HEADER;
+ }
return TYPE_DEFAULT;
}
}
@@ -171,7 +175,7 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
/**
* Set the slice item for this view.
*/
- void setSliceItem(SliceItem slice);
+ void setSliceItem(SliceItem slice, boolean isHeader);
/**
* Set the color for the items in this view.
@@ -210,8 +214,8 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
case FORMAT_TEXT:
builder.append(i.getText());
break;
- case FORMAT_COLOR:
- builder.append(i.getColor());
+ case FORMAT_INT:
+ builder.append(i.getInt());
break;
}
}
diff --git a/androidx/app/slice/widget/LargeTemplateView.java b/androidx/app/slice/widget/LargeTemplateView.java
index 0160f59f..d0e1364d 100644
--- a/androidx/app/slice/widget/LargeTemplateView.java
+++ b/androidx/app/slice/widget/LargeTemplateView.java
@@ -20,16 +20,20 @@ import static android.app.slice.Slice.HINT_ACTIONS;
import static android.app.slice.Slice.HINT_LIST;
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_PARTIAL;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
+import static android.app.slice.Slice.SUBTYPE_COLOR;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static androidx.app.slice.core.SliceHints.HINT_SUMMARY;
+
import android.annotation.TargetApi;
import android.content.Context;
import android.support.annotation.RestrictTo;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
-import android.util.TypedValue;
+import android.view.View;
+import android.widget.FrameLayout;
import java.util.ArrayList;
import java.util.List;
@@ -38,18 +42,18 @@ import java.util.function.Consumer;
import androidx.app.slice.Slice;
import androidx.app.slice.SliceItem;
import androidx.app.slice.core.SliceQuery;
+import androidx.app.slice.view.R;
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TargetApi(24)
-public class LargeTemplateView extends SliceView.SliceModeView {
+public class LargeTemplateView extends FrameLayout implements SliceView.SliceModeView {
private final LargeSliceAdapter mAdapter;
private final RecyclerView mRecyclerView;
private final int mDefaultHeight;
- private final int mMaxHeight;
private Slice mSlice;
private boolean mIsScrollable;
@@ -61,10 +65,12 @@ public class LargeTemplateView extends SliceView.SliceModeView {
mAdapter = new LargeSliceAdapter(context);
mRecyclerView.setAdapter(mAdapter);
addView(mRecyclerView);
- mDefaultHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200,
- getResources().getDisplayMetrics());
- mMaxHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200,
- getResources().getDisplayMetrics());
+ mDefaultHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_large_height);
+ }
+
+ @Override
+ public View getView() {
+ return this;
}
@Override
@@ -76,9 +82,10 @@ public class LargeTemplateView extends SliceView.SliceModeView {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mRecyclerView.getLayoutParams().height = WRAP_CONTENT;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- if (mRecyclerView.getMeasuredHeight() > mMaxHeight
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ if (mRecyclerView.getMeasuredHeight() > width
|| (mSlice != null && SliceQuery.hasHints(mSlice, HINT_PARTIAL))) {
- mRecyclerView.getLayoutParams().height = mDefaultHeight;
+ mRecyclerView.getLayoutParams().height = width;
} else {
mRecyclerView.getLayoutParams().height = mRecyclerView.getMeasuredHeight();
}
@@ -87,7 +94,7 @@ public class LargeTemplateView extends SliceView.SliceModeView {
@Override
public void setSlice(Slice slice) {
- SliceItem color = SliceQuery.find(slice, FORMAT_COLOR);
+ SliceItem color = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
mSlice = slice;
final List<SliceItem> items = new ArrayList<>();
final boolean[] hasHeader = new boolean[1];
@@ -97,9 +104,9 @@ public class LargeTemplateView extends SliceView.SliceModeView {
slice.getItems().forEach(new Consumer<SliceItem>() {
@Override
public void accept(SliceItem item) {
- if (item.hasHint(HINT_ACTIONS)) {
+ if (item.hasAnyHints(HINT_ACTIONS, HINT_SUMMARY)) {
return;
- } else if (FORMAT_COLOR.equals(item.getFormat())) {
+ } else if (FORMAT_INT.equals(item.getFormat())) {
return;
} else if (FORMAT_SLICE.equals(item.getFormat())
&& item.hasHint(HINT_LIST)) {
diff --git a/androidx/app/slice/widget/MessageView.java b/androidx/app/slice/widget/MessageView.java
index cb9497a8..e2678e16 100644
--- a/androidx/app/slice/widget/MessageView.java
+++ b/androidx/app/slice/widget/MessageView.java
@@ -60,7 +60,7 @@ public class MessageView extends LinearLayout implements LargeSliceAdapter.Slice
}
@Override
- public void setSliceItem(SliceItem slice) {
+ public void setSliceItem(SliceItem slice, boolean isHeader) {
SliceItem source = SliceQuery.findSubtype(slice, FORMAT_IMAGE, SUBTYPE_SOURCE);
if (source != null) {
final int iconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
diff --git a/androidx/app/slice/widget/SmallTemplateView.java b/androidx/app/slice/widget/RowView.java
index 1d3ab2d8..e7cd535f 100644
--- a/androidx/app/slice/widget/SmallTemplateView.java
+++ b/androidx/app/slice/widget/RowView.java
@@ -21,28 +21,34 @@ import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_NO_TINT;
import static android.app.slice.Slice.HINT_SELECTED;
import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.Slice.SUBTYPE_COLOR;
import static android.app.slice.SliceItem.FORMAT_ACTION;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
+import static androidx.app.slice.core.SliceHints.EXTRA_TOGGLE_STATE;
+import static androidx.app.slice.core.SliceHints.HINT_SUMMARY;
+
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
+import android.graphics.drawable.Icon;
import android.os.AsyncTask;
import android.support.annotation.RestrictTo;
import android.util.Log;
import android.view.View;
import android.widget.CompoundButton;
-import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Switch;
import android.widget.TextView;
+import android.widget.ToggleButton;
import java.util.ArrayList;
import java.util.List;
@@ -55,36 +61,38 @@ import androidx.app.slice.core.SliceQuery;
import androidx.app.slice.view.R;
/**
- * Small template is also used to construct list items for use with {@link LargeTemplateView}.
+ * Row item is in small template format and can be used to construct list items for use
+ * with {@link LargeTemplateView}.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TargetApi(23)
-public class SmallTemplateView extends SliceView.SliceModeView implements
+public class RowView extends FrameLayout implements SliceView.SliceModeView,
LargeSliceAdapter.SliceListView, View.OnClickListener {
- private static final String TAG = "SmallTemplateView";
+ private static final String TAG = "RowView";
// The number of items that fit on the right hand side of a small slice
private static final int MAX_END_ITEMS = 3;
- private SliceItem mColorItem;
-
private int mIconSize;
private int mPadding;
+ private boolean mInSmallMode;
+ private boolean mIsHeader;
private LinearLayout mStartContainer;
private LinearLayout mContent;
private TextView mPrimaryText;
private TextView mSecondaryText;
+ private View mDivider;
+ private CompoundButton mToggle;
private LinearLayout mEndContainer;
+ private SliceItem mColorItem;
private SliceItem mRowAction;
- private View mDivider;
- private Switch mToggle;
- public SmallTemplateView(Context context) {
+ public RowView(Context context) {
super(context);
mIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.abc_slice_icon_size);
mPadding = getContext().getResources().getDimensionPixelSize(R.dimen.abc_slice_padding);
@@ -99,6 +107,11 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
}
@Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
public @SliceView.SliceMode int getMode() {
return SliceView.MODE_SMALL;
}
@@ -108,34 +121,59 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
mColorItem = color;
}
-
+ /**
+ * This is called when RowView is being used as a component in a large template.
+ */
@Override
- public void setSliceItem(SliceItem slice) {
+ public void setSliceItem(SliceItem slice, boolean isHeader) {
+ mIsHeader = isHeader;
+ mInSmallMode = false;
populateViews(slice, slice);
}
+ /**
+ * This is called when RowView is being used as a small template.
+ */
@Override
public void setSlice(Slice slice) {
+ mInSmallMode = true;
Slice.Builder sb = new Slice.Builder(slice.getUri());
sb.addSubSlice(slice);
Slice parentSlice = sb.build();
- populateViews(parentSlice.getItems().get(0), getFirstSlice(slice));
+ populateViews(parentSlice.getItems().get(0), getSummaryItem(slice));
}
- private SliceItem getFirstSlice(Slice slice) {
+ private SliceItem getSummaryItem(Slice slice) {
List<SliceItem> items = slice.getItems();
- if (items.size() > 0 && FORMAT_SLICE.equals(items.get(0).getFormat())) {
+ // See if a summary is specified
+ SliceItem summary = SliceQuery.find(slice, FORMAT_SLICE, HINT_SUMMARY, null);
+ if (summary != null) {
+ return summary;
+ }
+ // First fallback is using a header
+ SliceItem header = SliceQuery.find(slice, FORMAT_SLICE, null, HINT_LIST_ITEM);
+ if (header != null) {
+ return header;
+ }
+ // Otherwise use the first non-color item and use it if it's a slice
+ SliceItem firstSlice = null;
+ for (int i = 0; i < items.size(); i++) {
+ if (!FORMAT_INT.equals(items.get(i).getFormat())) {
+ firstSlice = items.get(i);
+ break;
+ }
+ }
+ if (firstSlice != null && FORMAT_SLICE.equals(firstSlice.getFormat())) {
// Check if this slice is appropriate to use to populate small template
- SliceItem firstSlice = items.get(0);
if (firstSlice.hasHint(HINT_LIST)) {
// Check for header, use that if it exists
- SliceItem header = SliceQuery.find(firstSlice, FORMAT_SLICE,
+ SliceItem listHeader = SliceQuery.find(firstSlice, FORMAT_SLICE,
null,
new String[] {
HINT_LIST_ITEM, HINT_LIST
});
- if (header != null) {
- return SliceQuery.findFirstSlice(header);
+ if (listHeader != null) {
+ return SliceQuery.findFirstSlice(listHeader);
} else {
// Otherwise use the first list item
SliceItem newFirst = firstSlice.getSlice().getItems().get(0);
@@ -146,7 +184,7 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
return SliceQuery.findFirstSlice(firstSlice);
}
}
- // Get it as a SliceItem type slice
+ // Fallback, just use this and convert to SliceItem type slice
Slice.Builder sb = new Slice.Builder(slice.getUri());
Slice s = sb.addSubSlice(slice).build();
return s.getItems().get(0);
@@ -168,7 +206,7 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
SliceItem subTitle = null;
ArrayList<SliceItem> endItems = new ArrayList<>();
- // If the first item is an action let's check if it should be used to populate the content
+ // If the first item is an action check if it should be used to populate the content
// or if it should be in the start position.
SliceItem firstSlice = items.size() > 0 ? items.get(0) : null;
if (firstSlice != null && FORMAT_ACTION.equals(firstSlice.getFormat())) {
@@ -177,9 +215,6 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
items.remove(0);
// Populating with first action, bias to use slice associated with this action
items.addAll(0, mRowAction.getSlice().getItems());
- } else {
- // It's simple so maybe it's a start item
- startItem = items.get(0);
}
}
@@ -188,7 +223,9 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
SliceItem item = items.get(i);
List<String> hints = item.getHints();
String itemType = item.getFormat();
- if (hints.contains(HINT_TITLE)) {
+ if (i == 0 && SliceQuery.isStartType((item))) {
+ startItem = item;
+ } else if (hints.contains(HINT_TITLE)) {
// Things with these hints could go in the title / start position
if ((startItem == null || !startItem.hasHint(HINT_TITLE))
&& SliceQuery.isStartType(item)) {
@@ -217,9 +254,23 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
}
}
+ SliceItem colorItem = SliceQuery.findSubtype(fullSlice, FORMAT_INT, SUBTYPE_COLOR);
+ int color = colorItem != null
+ ? colorItem.getInt()
+ : (mColorItem != null)
+ ? mColorItem.getInt()
+ : -1;
// Populate main part of the template
- if (startItem != null) {
- // TODO - check for icon, timestamp, action with icon
+ if (!mIsHeader && !mInSmallMode && startItem != null) {
+ startItem = addItem(startItem, color, mStartContainer, 0 /* padding */)
+ ? startItem
+ : null;
+ if (startItem != null) {
+ endItems.remove(startItem);
+ }
+ } else if (startItem != null) {
+ endItems.add(0, startItem);
+ startItem = null;
}
mStartContainer.setVisibility(startItem != null ? View.VISIBLE : View.GONE);
if (titleItem != null) {
@@ -233,8 +284,8 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
// Figure out what end items we're showing
// If we're showing an action in this row check if it's a toggle
- if (mRowAction != null && SliceQuery.hasHints(mRowAction.getSlice(), SliceHints.HINT_TOGGLE)
- && addToggle(mRowAction)) {
+ if (mRowAction != null && SliceQuery.hasHints(mRowAction.getSlice(),
+ SliceHints.SUBTYPE_TOGGLE) && addToggle(mRowAction, color)) {
// Can't show more end actions if we have a toggle so we're done
makeClickable(this);
return;
@@ -245,47 +296,31 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
@Override
public boolean test(SliceItem item) {
return FORMAT_ACTION.equals(item.getFormat())
- && SliceQuery.hasHints(item.getSlice(), SliceHints.HINT_TOGGLE);
+ && SliceQuery.hasHints(item.getSlice(), SliceHints.SUBTYPE_TOGGLE);
}
- })
- .findFirst().orElse(null);
+ }).findFirst().orElse(null);
if (toggleItem != null) {
- if (addToggle(toggleItem)) {
+ if (addToggle(toggleItem, color)) {
mDivider.setVisibility(mRowAction != null ? View.VISIBLE : View.GONE);
makeClickable(mRowAction != null ? mContent : this);
// Can't show more end actions if we have a toggle so we're done
return;
}
}
- // If we're here we can still show end items
- SliceItem colorItem = SliceQuery.find(fullSlice, FORMAT_COLOR);
- int color = colorItem != null
- ? colorItem.getColor()
- : (mColorItem != null)
- ? mColorItem.getColor()
- : -1;
boolean clickableEndItem = false;
int itemCount = 0;
- for (int i = 0; i < items.size(); i++) {
- SliceItem item = items.get(i);
- if (itemCount <= MAX_END_ITEMS) {
- if (FORMAT_ACTION.equals(item.getFormat())) {
- if (SliceQuery.hasHints(item.getSlice(), SliceHints.HINT_TOGGLE)) {
- if (addToggle(item)) {
- break;
- }
- }
- if (addIcon(item, color, mEndContainer)) {
- clickableEndItem = true;
- itemCount++;
- }
- } else if (FORMAT_IMAGE.equals(item.getFormat())) {
- addIcon(item, color, mEndContainer);
- itemCount++;
- } else if (FORMAT_TIMESTAMP.equals(item.getFormat())) {
- TextView tv = new TextView(getContext());
- tv.setText(SliceViewUtil.getRelativeTimeString(item.getTimestamp()));
- mEndContainer.addView(tv);
+ for (int i = 0; i < endItems.size(); i++) {
+ SliceItem item = endItems.get(i);
+ // Only show one type of format at the end of the slice, use whatever is first
+ if (itemCount <= MAX_END_ITEMS
+ && item.getFormat().equals(endItems.get(0).getFormat())) {
+ if (FORMAT_ACTION.equals(item.getFormat())
+ && itemCount == 0
+ && SliceQuery.hasHints(item.getSlice(), SliceHints.SUBTYPE_TOGGLE)
+ && addToggle(item, color)) {
+ // If a toggle is added we're done
+ break;
+ } else if (addItem(item, color, mEndContainer, mPadding)) {
itemCount++;
}
}
@@ -298,20 +333,44 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
/**
* @return Whether a toggle was added.
*/
- private boolean addToggle(final SliceItem toggleItem) {
+ private boolean addToggle(final SliceItem toggleItem, int color) {
if (!FORMAT_ACTION.equals(toggleItem.getFormat())
- || !SliceQuery.hasHints(toggleItem.getSlice(), SliceHints.HINT_TOGGLE)) {
+ || !SliceQuery.hasHints(toggleItem.getSlice(), SliceHints.SUBTYPE_TOGGLE)) {
return false;
}
- mToggle = new Switch(getContext());
- mEndContainer.addView(mToggle);
+
+ // Check if this is a custom toggle
+ Icon checkedIcon = null;
+ List<SliceItem> sliceItems = toggleItem.getSlice().getItems();
+ if (sliceItems.size() > 0) {
+ checkedIcon = FORMAT_IMAGE.equals(sliceItems.get(0).getFormat())
+ ? sliceItems.get(0).getIcon()
+ : null;
+ }
+ if (checkedIcon != null) {
+ if (color != -1) {
+ // TODO - Should these be tinted? What if the app wants diff colors per state?
+ checkedIcon.setTint(color);
+ }
+ mToggle = new ToggleButton(getContext());
+ ((ToggleButton) mToggle).setTextOff("");
+ ((ToggleButton) mToggle).setTextOn("");
+ mToggle.setBackground(checkedIcon.loadDrawable(getContext()));
+ mEndContainer.addView(mToggle);
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mToggle.getLayoutParams();
+ lp.width = mIconSize;
+ lp.height = mIconSize;
+ } else {
+ mToggle = new Switch(getContext());
+ mEndContainer.addView(mToggle);
+ }
mToggle.setChecked(SliceQuery.hasHints(toggleItem.getSlice(), HINT_SELECTED));
- mToggle.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ mToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
try {
PendingIntent pi = toggleItem.getAction();
- Intent i = new Intent().putExtra(SliceHints.EXTRA_TOGGLE_STATE, isChecked);
+ Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
pi.send(getContext(), 0, i, null, null);
} catch (CanceledException e) {
mToggle.setSelected(!isChecked);
@@ -322,40 +381,29 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
}
/**
- * @return Whether an icon was added.
+ * Adds simple items to a container. Simple items include actions with icons, images, or
+ * timestamps.
+ *
+ * @return Whether an item was added to the view.
*/
- private boolean addIcon(SliceItem sliceItem, int color, LinearLayout container) {
+ private boolean addItem(SliceItem sliceItem, int color, LinearLayout container, int padding) {
SliceItem image = null;
SliceItem action = null;
- if (FORMAT_ACTION.equals(sliceItem.getFormat())) {
+ SliceItem timeStamp = null;
+ if (FORMAT_ACTION.equals(sliceItem.getFormat())
+ && !sliceItem.hasHint(SliceHints.SUBTYPE_TOGGLE)) {
image = SliceQuery.find(sliceItem.getSlice(), FORMAT_IMAGE);
+ timeStamp = SliceQuery.find(sliceItem.getSlice(), FORMAT_TIMESTAMP);
action = sliceItem;
} else if (FORMAT_IMAGE.equals(sliceItem.getFormat())) {
image = sliceItem;
+ } else if (FORMAT_TIMESTAMP.equals(sliceItem.getFormat())) {
+ timeStamp = sliceItem;
}
+ View addedView = null;
if (image != null) {
ImageView iv = new ImageView(getContext());
iv.setImageIcon(image.getIcon());
- if (action != null) {
- final SliceItem sliceAction = action;
- iv.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- AsyncTask.execute(new Runnable() {
- @Override
- public void run() {
- try {
- sliceAction.getAction().send();
- } catch (CanceledException e) {
- e.printStackTrace();
- }
- }
- });
- }
- });
- iv.setBackground(SliceViewUtil.getDrawable(getContext(),
- android.R.attr.selectableItemBackground));
- }
if (color != -1 && !sliceItem.hasHint(HINT_NO_TINT)) {
iv.setColorFilter(color);
}
@@ -363,20 +411,41 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) iv.getLayoutParams();
lp.width = mIconSize;
lp.height = mIconSize;
- lp.setMarginStart(mPadding);
- return true;
+ lp.setMarginStart(padding);
+ addedView = iv;
+ } else if (timeStamp != null) {
+ TextView tv = new TextView(getContext());
+ tv.setText(SliceViewUtil.getRelativeTimeString(sliceItem.getTimestamp()));
+ container.addView(tv);
+ addedView = tv;
}
- return false;
+ if (action != null && addedView != null) {
+ final SliceItem sliceAction = action;
+ addedView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ AsyncTask.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sliceAction.getAction().send();
+ } catch (CanceledException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+ });
+ addedView.setBackground(SliceViewUtil.getDrawable(getContext(),
+ android.R.attr.selectableItemBackground));
+ }
+ return addedView != null;
}
@Override
public void onClick(View view) {
if (mRowAction != null && FORMAT_ACTION.equals(mRowAction.getFormat())) {
- if (mToggle != null
- && SliceQuery.hasHints(mRowAction.getSlice(), SliceHints.HINT_TOGGLE)) {
- mToggle.toggle();
- return;
- }
+ // Check for a row action
AsyncTask.execute(new Runnable() {
@Override
public void run() {
@@ -387,6 +456,9 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
}
}
});
+ } else if (mToggle != null) {
+ // Or no row action so let's just toggle if we've got one
+ mToggle.toggle();
}
}
@@ -401,5 +473,6 @@ public class SmallTemplateView extends SliceView.SliceModeView implements
mEndContainer.removeAllViews();
mPrimaryText.setText(null);
mSecondaryText.setText(null);
+ mDivider.setVisibility(View.GONE);
}
}
diff --git a/androidx/app/slice/widget/ShortcutView.java b/androidx/app/slice/widget/ShortcutView.java
index f93abc6d..1b88d545 100644
--- a/androidx/app/slice/widget/ShortcutView.java
+++ b/androidx/app/slice/widget/ShortcutView.java
@@ -18,10 +18,11 @@ package androidx.app.slice.widget;
import static android.app.slice.Slice.HINT_LARGE;
import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.Slice.SUBTYPE_COLOR;
import static android.app.slice.Slice.SUBTYPE_SOURCE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import android.annotation.TargetApi;
@@ -39,6 +40,8 @@ import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.net.Uri;
import android.support.annotation.RestrictTo;
+import android.view.View;
+import android.widget.FrameLayout;
import androidx.app.slice.Slice;
import androidx.app.slice.SliceItem;
@@ -50,7 +53,7 @@ import androidx.app.slice.view.R;
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TargetApi(23)
-public class ShortcutView extends androidx.app.slice.widget.SliceView.SliceModeView {
+public class ShortcutView extends FrameLayout implements SliceView.SliceModeView {
private static final String TAG = "ShortcutView";
@@ -70,18 +73,23 @@ public class ShortcutView extends androidx.app.slice.widget.SliceView.SliceModeV
}
@Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
public void setSlice(Slice slice) {
mLabel = null;
mIcon = null;
mAction = null;
removeAllViews();
determineShortcutItems(getContext(), slice);
- SliceItem colorItem = SliceQuery.find(slice, FORMAT_COLOR);
+ SliceItem colorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
if (colorItem == null) {
- colorItem = SliceQuery.find(slice, FORMAT_COLOR);
+ colorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
}
// TODO: pick better default colour
- final int color = colorItem != null ? colorItem.getColor() : Color.GRAY;
+ final int color = colorItem != null ? colorItem.getInt() : Color.GRAY;
ShapeDrawable circle = new ShapeDrawable(new OvalShape());
circle.setTint(color);
setBackground(circle);
diff --git a/androidx/app/slice/widget/SliceLiveData.java b/androidx/app/slice/widget/SliceLiveData.java
index 9b36ee12..8ef221a5 100644
--- a/androidx/app/slice/widget/SliceLiveData.java
+++ b/androidx/app/slice/widget/SliceLiveData.java
@@ -24,7 +24,11 @@ import android.os.AsyncTask;
import android.os.Handler;
import android.support.annotation.NonNull;
+import java.util.Arrays;
+import java.util.List;
+
import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpec;
/**
* Class with factory methods for creating LiveData that observes slices.
@@ -34,6 +38,8 @@ import androidx.app.slice.Slice;
*/
public final class SliceLiveData {
+ private static final List<SliceSpec> SUPPORTED_SPECS = Arrays.asList();
+
/**
* Produces an {@link LiveData} that tracks a Slice for a given Uri. To use
* this method your app must have the permission to the slice Uri or hold
diff --git a/androidx/app/slice/widget/SliceView.java b/androidx/app/slice/widget/SliceView.java
index 5597ac9d..951bdd5d 100644
--- a/androidx/app/slice/widget/SliceView.java
+++ b/androidx/app/slice/widget/SliceView.java
@@ -17,7 +17,9 @@
package androidx.app.slice.widget;
import static android.app.slice.Slice.HINT_ACTIONS;
-import static android.app.slice.SliceItem.FORMAT_COLOR;
+import static android.app.slice.Slice.HINT_HORIZONTAL;
+import static android.app.slice.Slice.SUBTYPE_COLOR;
+import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import android.arch.lifecycle.Observer;
@@ -32,7 +34,6 @@ import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.FrameLayout;
import java.util.List;
@@ -80,21 +81,22 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
- public abstract static class SliceModeView extends FrameLayout {
-
- public SliceModeView(Context context) {
- super(context);
- }
+ public interface SliceModeView {
/**
* @return the mode of the slice being presented.
*/
- public abstract int getMode();
+ int getMode();
/**
* @param slice the slice to show in this view.
*/
- public abstract void setSlice(Slice slice);
+ void setSlice(Slice slice);
+
+ /**
+ * @return the view.
+ */
+ View getView();
}
/**
@@ -152,7 +154,7 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
mActions = new ActionRow(getContext(), true);
mActions.setBackground(new ColorDrawable(0xffeeeeee));
mCurrentView = new LargeTemplateView(getContext());
- addView(mCurrentView, getChildLp(mCurrentView));
+ addView(mCurrentView.getView(), getChildLp(mCurrentView.getView()));
addView(mActions, getChildLp(mActions));
mShortcutSize = getContext().getResources()
.getDimensionPixelSize(R.dimen.abc_slice_shortcut_size);
@@ -173,17 +175,18 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
? mActions.getMeasuredHeight()
: 0;
int newHeightSpec = MeasureSpec.makeMeasureSpec(
- mCurrentView.getMeasuredHeight() + actionHeight, MeasureSpec.EXACTLY);
+ mCurrentView.getView().getMeasuredHeight() + actionHeight, MeasureSpec.EXACTLY);
setMeasuredDimension(width, newHeightSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
- mCurrentView.layout(0, 0, mCurrentView.getMeasuredWidth(),
- mCurrentView.getMeasuredHeight());
+ View v = mCurrentView.getView();
+ v.layout(0, 0, v.getMeasuredWidth(),
+ v.getMeasuredHeight());
if (mActions.getVisibility() != View.GONE) {
- mActions.layout(0, mCurrentView.getMeasuredHeight(), mActions.getMeasuredWidth(),
- mCurrentView.getMeasuredHeight() + mActions.getMeasuredHeight());
+ mActions.layout(0, v.getMeasuredHeight(), mActions.getMeasuredWidth(),
+ v.getMeasuredHeight() + mActions.getMeasuredHeight());
}
}
@@ -256,7 +259,12 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
case MODE_SHORTCUT:
return new ShortcutView(getContext());
case MODE_SMALL:
- return new SmallTemplateView(getContext());
+ // Check if it's horizontal
+ if (SliceQuery.hasHints(mCurrentSlice, HINT_HORIZONTAL)) {
+ return new GridRowView(getContext());
+ } else {
+ return new RowView(getContext());
+ }
}
return new LargeTemplateView(getContext());
}
@@ -266,7 +274,7 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
return;
}
// TODO: Smarter mapping here from one state to the next.
- SliceItem color = SliceQuery.find(mCurrentSlice, FORMAT_COLOR);
+ SliceItem color = SliceQuery.findSubtype(mCurrentSlice, FORMAT_INT, SUBTYPE_COLOR);
List<SliceItem> items = mCurrentSlice.getItems();
SliceItem actionRow = SliceQuery.find(mCurrentSlice, FORMAT_SLICE,
HINT_ACTIONS,
@@ -277,17 +285,17 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
} else {
removeAllViews();
mCurrentView = createView(mode);
- addView(mCurrentView, getChildLp(mCurrentView));
+ addView(mCurrentView.getView(), getChildLp(mCurrentView.getView()));
addView(mActions, getChildLp(mActions));
}
if (mode == MODE_LARGE) {
((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable);
}
if (items.size() > 1 || (items.size() != 0 && items.get(0) != actionRow)) {
- mCurrentView.setVisibility(View.VISIBLE);
+ mCurrentView.getView().setVisibility(View.VISIBLE);
mCurrentView.setSlice(mCurrentSlice);
} else {
- mCurrentView.setVisibility(View.GONE);
+ mCurrentView.getView().setVisibility(View.GONE);
}
boolean showActions = mShowActions && actionRow != null
diff --git a/android/support/car/drawer/CarDrawerActivity.java b/androidx/car/drawer/CarDrawerActivity.java
index f46c652b..9769142a 100644
--- a/android/support/car/drawer/CarDrawerActivity.java
+++ b/androidx/car/drawer/CarDrawerActivity.java
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package android.support.car.drawer;
+package androidx.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;
@@ -30,13 +29,16 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import androidx.car.R;
+
/**
* 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>Provide the root-items for the drawer by calling {@link #getDrawerController()}.
+ * {@link CarDrawerController#setRootAdapter(CarDrawerAdapter)}.
* <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()}
@@ -51,7 +53,7 @@ import android.view.ViewGroup;
* <p>Any Activity's based on this class need to set their theme to CarDrawerActivityTheme or a
* derivative.
*/
-public abstract class CarDrawerActivity extends AppCompatActivity {
+public class CarDrawerActivity extends AppCompatActivity {
private CarDrawerController mDrawerController;
@Override
@@ -71,7 +73,10 @@ public abstract class CarDrawerActivity extends AppCompatActivity {
setSupportActionBar(toolbar);
mDrawerController = new CarDrawerController(toolbar, drawerLayout, drawerToggle);
- mDrawerController.setRootAdapter(getRootAdapter());
+ CarDrawerAdapter rootAdapter = getRootAdapter();
+ if (rootAdapter != null) {
+ mDrawerController.setRootAdapter(rootAdapter);
+ }
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
@@ -97,8 +102,14 @@ public abstract class CarDrawerActivity extends AppCompatActivity {
/**
* @return Adapter for root content of the Drawer.
+ * @deprecated Do not implement this, instead call {@link #getDrawerController}.
+ * {@link CarDrawerController#setRootAdapter(CarDrawerAdapter)} directly.
*/
- protected abstract CarDrawerAdapter getRootAdapter();
+ @Deprecated
+ @Nullable
+ protected CarDrawerAdapter getRootAdapter() {
+ return null;
+ }
/**
* Set main content to display in this Activity. It will be added to R.id.content_frame in
diff --git a/android/support/car/drawer/CarDrawerAdapter.java b/androidx/car/drawer/CarDrawerAdapter.java
index b0fd965d..ca16413e 100644
--- a/android/support/car/drawer/CarDrawerAdapter.java
+++ b/androidx/car/drawer/CarDrawerAdapter.java
@@ -14,20 +14,21 @@
* limitations under the License.
*/
-package android.support.car.drawer;
+package androidx.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;
+import androidx.car.R;
+import androidx.car.widget.PagedListView;
+
/**
* Base adapter for displaying items in the car navigation drawer, which uses a
* {@link PagedListView}.
diff --git a/android/support/car/drawer/CarDrawerController.java b/androidx/car/drawer/CarDrawerController.java
index 7b23714c..e26054fe 100644
--- a/android/support/car/drawer/CarDrawerController.java
+++ b/androidx/car/drawer/CarDrawerController.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.support.car.drawer;
+package androidx.car.drawer;
import android.content.Context;
import android.content.res.Configuration;
@@ -22,8 +22,6 @@ 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;
@@ -34,7 +32,10 @@ import android.view.View;
import android.view.animation.AnimationUtils;
import android.widget.ProgressBar;
-import java.util.Stack;
+import java.util.ArrayDeque;
+
+import androidx.car.R;
+import androidx.car.widget.PagedListView;
/**
* A controller that will handle the set up of the navigation drawer. It will hook up the
@@ -58,7 +59,7 @@ public class CarDrawerController {
* 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 ArrayDeque<CarDrawerAdapter> mAdapterStack = new ArrayDeque<>();
private final Context mContext;
@@ -114,11 +115,10 @@ public class CarDrawerController {
}
// The root adapter is always the last item in the stack.
- if (mAdapterStack.size() > 0) {
- mAdapterStack.set(0, rootAdapter);
- } else {
- mAdapterStack.push(rootAdapter);
+ if (!mAdapterStack.isEmpty()) {
+ mAdapterStack.removeLast();
}
+ mAdapterStack.addLast(rootAdapter);
setToolbarTitleFrom(rootAdapter);
mDrawerList.setAdapter(rootAdapter);
diff --git a/android/support/car/drawer/DrawerItemClickListener.java b/androidx/car/drawer/DrawerItemClickListener.java
index d707dbd0..4c0c7a2a 100644
--- a/android/support/car/drawer/DrawerItemClickListener.java
+++ b/androidx/car/drawer/DrawerItemClickListener.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.support.car.drawer;
+package androidx.car.drawer;
/**
* Listener for handling clicks on items/views managed by {@link DrawerItemViewHolder}.
diff --git a/android/support/car/drawer/DrawerItemViewHolder.java b/androidx/car/drawer/DrawerItemViewHolder.java
index d016b2de..8bbded99 100644
--- a/android/support/car/drawer/DrawerItemViewHolder.java
+++ b/androidx/car/drawer/DrawerItemViewHolder.java
@@ -14,19 +14,20 @@
* limitations under the License.
*/
-package android.support.car.drawer;
+package androidx.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;
+import androidx.car.R;
+
/**
* Re-usable {@link RecyclerView.ViewHolder} for displaying items in the
- * {@link android.support.car.drawer.CarDrawerAdapter}.
+ * {@link androidx.car.drawer.CarDrawerAdapter}.
*/
public class DrawerItemViewHolder extends RecyclerView.ViewHolder {
private final ImageView mIcon;
diff --git a/android/support/car/utils/ColumnCalculator.java b/androidx/car/utils/ColumnCalculator.java
index fa5dd432..35b1a917 100644
--- a/android/support/car/utils/ColumnCalculator.java
+++ b/androidx/car/utils/ColumnCalculator.java
@@ -14,15 +14,16 @@
* limitations under the License.
*/
-package android.support.car.utils;
+package androidx.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;
+import androidx.car.R;
+
/**
* 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
@@ -66,8 +67,8 @@ public class ColumnCalculator {
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);
+ mGutterSize = res.getDimensionPixelSize(R.dimen.car_gutter_size);
+ mNumOfColumns = res.getInteger(R.integer.car_column_number);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("marginSize: %d; numOfColumns: %d; gutterSize: %d",
diff --git a/androidx/car/utils/ListItemBackgroundResolver.java b/androidx/car/utils/ListItemBackgroundResolver.java
new file mode 100644
index 00000000..406c0478
--- /dev/null
+++ b/androidx/car/utils/ListItemBackgroundResolver.java
@@ -0,0 +1,70 @@
+/*
+ * 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 androidx.car.utils;
+
+import android.view.View;
+
+import androidx.car.R;
+
+/**
+ * A utility class that will set the current background for a View that represents an card entry
+ * in a list. The class will set the background depending on the position of the card within the
+ * list.
+ */
+public class ListItemBackgroundResolver {
+ private ListItemBackgroundResolver() {}
+
+ /**
+ * Sets the background on the given view so that the combination of all items looks like a
+ * rectangle with rounded corner. The view is assumed to have a non-rounded corner outline.
+ *
+ * <p>The view will be set with rounded backgrounds if it is the only card within the list.
+ * Or if it is the first or last view, it will have the top or bottom corners rounded
+ * respectively.
+ *
+ * @param view The view whose background to set.
+ * @param currentPosition The current position of the View within the list. This value should
+ * be 0-based.
+ * @param totalItems The total items within the list.
+ */
+ public static void setBackground(View view, int currentPosition, int totalItems) {
+ if (currentPosition < 0) {
+ throw new IllegalArgumentException("currentPosition cannot be less than zero.");
+ }
+
+ if (currentPosition >= totalItems) {
+ throw new IndexOutOfBoundsException("currentPosition: " + currentPosition + "; "
+ + "totalItems: " + totalItems);
+ }
+
+ // Set the background for each card. Only the top and last card should have rounded corners.
+ if (totalItems == 1) {
+ // One card - all corners are rounded.
+ view.setBackgroundResource(R.drawable.car_card_rounded_background);
+ } else if (currentPosition == 0) {
+ // First card gets rounded top.
+ view.setBackgroundResource(R.drawable.car_card_rounded_top_background);
+ } else if (currentPosition == totalItems - 1) {
+ // Last one has a rounded bottom.
+ view.setBackgroundResource(R.drawable.car_card_rounded_bottom_background);
+ } else {
+ // Middle has no rounded corners.
+ view.setBackgroundResource(R.drawable.car_card_background);
+ }
+ }
+}
+
diff --git a/androidx/car/utils/ListItemBackgroundResolverTest.java b/androidx/car/utils/ListItemBackgroundResolverTest.java
new file mode 100644
index 00000000..b809c79b
--- /dev/null
+++ b/androidx/car/utils/ListItemBackgroundResolverTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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 androidx.car.utils;
+
+
+import static org.mockito.Mockito.verify;
+
+import android.view.View;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import androidx.car.R;
+
+/**
+ * Tests for {@link ListItemBackgroundResolver}.
+ */
+@RunWith(JUnit4.class)
+public class ListItemBackgroundResolverTest {
+
+ @Rule
+ public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock
+ View mMockView;
+
+ @Test
+ public void testSingleItemInListHasAllRoundedCorners() {
+ ListItemBackgroundResolver.setBackground(mMockView, 0, 1);
+
+ verify(mMockView).setBackgroundResource(R.drawable.car_card_rounded_background);
+ }
+
+ @Test
+ public void testOnlyTopItemHasTopRoundedCorners() {
+ ListItemBackgroundResolver.setBackground(mMockView, 0, 2);
+
+ verify(mMockView).setBackgroundResource(R.drawable.car_card_rounded_top_background);
+ }
+
+ @Test
+ public void testOnlyBottomItemHasBottomRoundedCorners() {
+ ListItemBackgroundResolver.setBackground(mMockView, 1, 2);
+
+ verify(mMockView).setBackgroundResource(R.drawable.car_card_rounded_bottom_background);
+ }
+
+ @Test
+ public void testMiddleItemHasNoRoundedCorner() {
+ ListItemBackgroundResolver.setBackground(mMockView, 1, 3);
+
+ verify(mMockView).setBackgroundResource(R.drawable.car_card_background);
+ }
+}
diff --git a/android/support/car/widget/ColumnCardView.java b/androidx/car/widget/ColumnCardView.java
index 06f85536..9ec2bb65 100644
--- a/android/support/car/widget/ColumnCardView.java
+++ b/androidx/car/widget/ColumnCardView.java
@@ -14,16 +14,17 @@
* limitations under the License.
*/
-package android.support.car.widget;
+package androidx.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;
+import androidx.car.R;
+import androidx.car.utils.ColumnCalculator;
+
/**
* A {@link CardView} whose width can be specified by the number of columns that it will span.
*
@@ -34,7 +35,7 @@ import android.util.Log;
* a default span value that it uses.
*
* <pre>
- * &lt;android.support.car.widget.ColumnCardView
+ * &lt;androidx.car.widget.ColumnCardView
* android:layout_width="wrap_content"
* android:layout_height="wrap_content"
* app:columnSpan="4" /&gt;
diff --git a/android/support/car/widget/DayNightStyle.java b/androidx/car/widget/DayNightStyle.java
index ff5a1b33..6e3ecbef 100644
--- a/android/support/car/widget/DayNightStyle.java
+++ b/androidx/car/widget/DayNightStyle.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.support.car.widget;
+package androidx.car.widget;
import android.support.annotation.IntDef;
diff --git a/androidx/car/widget/ListItem.java b/androidx/car/widget/ListItem.java
new file mode 100644
index 00000000..d292d6b2
--- /dev/null
+++ b/androidx/car/widget/ListItem.java
@@ -0,0 +1,701 @@
+/*
+ * 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 androidx.car.widget;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.IntDef;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.RelativeLayout;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.car.R;
+
+/**
+ * Class to build a list item.
+ *
+ * <p>An item supports primary action and supplemental action(s).
+ *
+ * <p>An item visually composes of 3 parts; each part may contain multiple views.
+ * <ul>
+ * <li>{@code Primary Action}: represented by an icon of following types.
+ * <ul>
+ * <li>Primary Icon - icon size could be large or small.
+ * <li>No Icon
+ * <li>Empty Icon - different from No Icon by how much margin {@code Text} offsets
+ * </ul>
+ * <li>{@code Text}: supports any combination of the follow text views.
+ * <ul>
+ * <li>Title
+ * <li>Body
+ * </ul>
+ * <li>{@code Supplemental Action(s)}: represented by one of the following types; aligned toward
+ * the end of item.
+ * <ul>
+ * <li>Supplemental Icon
+ * <li>One Action Button
+ * <li>Two Action Buttons
+ * </ul>
+ * </ul>
+ *
+ * {@link ListItem} can be built through its {@link ListItem.Builder}. It binds data
+ * to {@link ListItemAdapter.ViewHolder} based on components selected.
+ */
+public class ListItem {
+
+ private Builder mBuilder;
+
+ private ListItem(Builder builder) {
+ mBuilder = builder;
+ }
+
+ /**
+ * Applies all {@link ViewBinder} to {@code viewHolder}.
+ */
+ void bind(ListItemAdapter.ViewHolder viewHolder) {
+ setAllSubViewsGone(viewHolder);
+ for (ViewBinder binder : mBuilder.mBinders) {
+ binder.bind(viewHolder);
+ }
+ }
+
+ void setAllSubViewsGone(ListItemAdapter.ViewHolder vh) {
+ View[] subviews = new View[] {
+ vh.getPrimaryIcon(),
+ vh.getTitle(), vh.getBody(),
+ vh.getSupplementalIcon(), vh.getSupplementalIconDivider(),
+ vh.getAction1(), vh.getAction1Divider(), vh.getAction2(), vh.getAction2Divider()};
+ for (View v : subviews) {
+ v.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Functional interface to provide a way to interact with views in
+ * {@link ListItemAdapter.ViewHolder}. {@code ViewBinder}s added to a
+ * {@code ListItem} will be called when {@code ListItem} {@code bind}s to
+ * {@link ListItemAdapter.ViewHolder}.
+ */
+ public interface ViewBinder {
+ /**
+ * Provides a way to interact with views in view holder.
+ */
+ void bind(ListItemAdapter.ViewHolder viewHolder);
+ }
+
+ /**
+ * Builds a {@link ListItem}.
+ *
+ * <p>With conflicting methods are called, e.g. setting primary action to both primary icon and
+ * no icon, the last called method wins.
+ */
+ public static class Builder {
+
+ @Retention(SOURCE)
+ @IntDef({
+ PRIMARY_ACTION_TYPE_NO_ICON, PRIMARY_ACTION_TYPE_EMPTY_ICON,
+ PRIMARY_ACTION_TYPE_LARGE_ICON, PRIMARY_ACTION_TYPE_SMALL_ICON})
+ private @interface PrimaryActionType {}
+
+ private static final int PRIMARY_ACTION_TYPE_NO_ICON = 0;
+ private static final int PRIMARY_ACTION_TYPE_EMPTY_ICON = 1;
+ private static final int PRIMARY_ACTION_TYPE_LARGE_ICON = 2;
+ private static final int PRIMARY_ACTION_TYPE_SMALL_ICON = 3;
+
+ @Retention(SOURCE)
+ @IntDef({SUPPLEMENTAL_ACTION_NO_ACTION, SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON,
+ SUPPLEMENTAL_ACTION_ONE_ACTION, SUPPLEMENTAL_ACTION_TWO_ACTIONS})
+ private @interface SupplementalActionType {}
+
+ private static final int SUPPLEMENTAL_ACTION_NO_ACTION = 0;
+ private static final int SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON = 1;
+ private static final int SUPPLEMENTAL_ACTION_ONE_ACTION = 2;
+ private static final int SUPPLEMENTAL_ACTION_TWO_ACTIONS = 3;
+
+ private final Context mContext;
+ private final List<ViewBinder> mBinders = new ArrayList<>();
+ // Store custom binders separately so they will bind after binders are created in build().
+ private final List<ViewBinder> mCustomBinders = new ArrayList<>();
+
+ private View.OnClickListener mOnClickListener;
+
+ @PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
+ private int mPrimaryActionIconResId;
+ private Drawable mPrimaryActionIconDrawable;
+
+ private String mTitle;
+ private String mBody;
+ private boolean mIsBodyPrimary;
+
+ @SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
+ private int mSupplementalIconResId;
+ private View.OnClickListener mSupplementalIconOnClickListener;
+ private boolean mShowSupplementalIconDivider;
+
+ private String mAction1Text;
+ private View.OnClickListener mAction1OnClickListener;
+ private boolean mShowAction1Divider;
+ private String mAction2Text;
+ private View.OnClickListener mAction2OnClickListener;
+ private boolean mShowAction2Divider;
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Builds a {@link ListItem}. Adds {@link ViewBinder}s that will adjust layout in
+ * {@link ListItemAdapter.ViewHolder} depending on sub-views used.
+ */
+ public ListItem build() {
+ setItemLayoutHeight();
+ setPrimaryAction();
+ setText();
+ setSupplementalActions();
+ setOnClickListener();
+
+ mBinders.addAll(mCustomBinders);
+
+ return new ListItem(this);
+ }
+
+ /**
+ * Sets the height of item depending on which text field is set.
+ */
+ private void setItemLayoutHeight() {
+ if (TextUtils.isEmpty(mBody)) {
+ // If the item only has title or no text, it uses fixed-height as single line.
+ int height = (int) mContext.getResources().getDimension(
+ R.dimen.car_single_line_list_item_height);
+ mBinders.add((vh) -> {
+ RecyclerView.LayoutParams layoutParams =
+ (RecyclerView.LayoutParams) vh.itemView.getLayoutParams();
+ layoutParams.height = height;
+ vh.itemView.setLayoutParams(layoutParams);
+ });
+ } else {
+ // If body is present, the item should be at least as tall as min height, and wraps
+ // content.
+ int minHeight = (int) mContext.getResources().getDimension(
+ R.dimen.car_double_line_list_item_height);
+ mBinders.add((vh) -> {
+ vh.itemView.setMinimumHeight(minHeight);
+ vh.getContainerLayout().setMinimumHeight(minHeight);
+
+ RecyclerView.LayoutParams layoutParams =
+ (RecyclerView.LayoutParams) vh.itemView.getLayoutParams();
+ layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT;
+ vh.itemView.setLayoutParams(layoutParams);
+ });
+ }
+ }
+
+ private void setPrimaryAction() {
+ setPrimaryIconContent();
+ setPrimaryIconLayout();
+ }
+
+ private void setText() {
+ setTextContent();
+ setTextVerticalMargin();
+ // Only setting start margin because text end is relative to the start of supplemental
+ // actions.
+ setTextStartMargin();
+ }
+
+ private void setOnClickListener() {
+ if (mOnClickListener != null) {
+ mBinders.add(vh -> vh.itemView.setOnClickListener(mOnClickListener));
+ }
+ }
+
+ private void setPrimaryIconContent() {
+ switch (mPrimaryActionType) {
+ case PRIMARY_ACTION_TYPE_SMALL_ICON:
+ case PRIMARY_ACTION_TYPE_LARGE_ICON:
+ mBinders.add((vh) -> {
+ vh.getPrimaryIcon().setVisibility(View.VISIBLE);
+
+ if (mPrimaryActionIconDrawable != null) {
+ vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
+ } else if (mPrimaryActionIconResId != 0) {
+ vh.getPrimaryIcon().setImageResource(mPrimaryActionIconResId);
+ }
+ });
+ break;
+ case PRIMARY_ACTION_TYPE_EMPTY_ICON:
+ case PRIMARY_ACTION_TYPE_NO_ICON:
+ // Do nothing.
+ break;
+ default:
+ throw new IllegalStateException("Unrecognizable primary action type.");
+ }
+ }
+
+ /**
+ * Sets layout params of primary icon.
+ *
+ * <p>Large icon will have no start margin, and always align center vertically.
+ *
+ * <p>Small icon will have start margin. When body text is present small icon uses a top
+ * margin otherwise align center vertically.
+ */
+ private void setPrimaryIconLayout() {
+ // Set all relevant fields in layout params to avoid carried over params when the item
+ // gets bound to a recycled view holder.
+ switch (mPrimaryActionType) {
+ case PRIMARY_ACTION_TYPE_SMALL_ICON:
+ mBinders.add(vh -> {
+ int iconSize = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_primary_icon_size);
+ // Icon size.
+ RelativeLayout.LayoutParams layoutParams =
+ (RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams();
+ layoutParams.height = iconSize;
+ layoutParams.width = iconSize;
+
+ // Start margin.
+ layoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_keyline_1));
+
+ if (!TextUtils.isEmpty(mBody)) {
+ // Set top margin.
+ layoutParams.removeRule(RelativeLayout.CENTER_VERTICAL);
+ layoutParams.topMargin = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_padding_4);
+ } else {
+ // Centered vertically.
+ layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+ layoutParams.topMargin = 0;
+ }
+ vh.getPrimaryIcon().setLayoutParams(layoutParams);
+ });
+ break;
+ case PRIMARY_ACTION_TYPE_LARGE_ICON:
+ mBinders.add(vh -> {
+ int iconSize = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_single_line_list_item_height);
+ // Icon size.
+ RelativeLayout.LayoutParams layoutParams =
+ (RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams();
+ layoutParams.height = iconSize;
+ layoutParams.width = iconSize;
+
+ // No start margin.
+ layoutParams.setMarginStart(0);
+
+ // Always centered vertically.
+ layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+ layoutParams.topMargin = 0;
+
+ vh.getPrimaryIcon().setLayoutParams(layoutParams);
+ });
+ break;
+ case PRIMARY_ACTION_TYPE_EMPTY_ICON:
+ case PRIMARY_ACTION_TYPE_NO_ICON:
+ // Do nothing.
+ break;
+ default:
+ throw new IllegalStateException("Unrecognizable primary action type.");
+ }
+ }
+
+ private void setTextContent() {
+ if (!TextUtils.isEmpty(mTitle)) {
+ mBinders.add(vh -> {
+ vh.getTitle().setVisibility(View.VISIBLE);
+ vh.getTitle().setText(mTitle);
+ });
+ }
+ if (!TextUtils.isEmpty(mBody)) {
+ mBinders.add(vh -> {
+ vh.getBody().setVisibility(View.VISIBLE);
+ vh.getBody().setText(mBody);
+ });
+ }
+
+ if (mIsBodyPrimary) {
+ mBinders.add((vh) -> {
+ vh.getTitle().setTextAppearance(R.style.CarBody2);
+ vh.getBody().setTextAppearance(R.style.CarBody1);
+ });
+ } else {
+ mBinders.add((vh) -> {
+ vh.getTitle().setTextAppearance(R.style.CarBody1);
+ vh.getBody().setTextAppearance(R.style.CarBody2);
+ });
+ }
+ }
+
+ /**
+ * Sets start margin of text view depending on icon type.
+ */
+ private void setTextStartMargin() {
+ final int startMarginResId;
+ switch (mPrimaryActionType) {
+ case PRIMARY_ACTION_TYPE_NO_ICON:
+ startMarginResId = R.dimen.car_keyline_1;
+ break;
+ case PRIMARY_ACTION_TYPE_EMPTY_ICON:
+ startMarginResId = R.dimen.car_keyline_3;
+ break;
+ case PRIMARY_ACTION_TYPE_SMALL_ICON:
+ startMarginResId = R.dimen.car_keyline_3;
+ break;
+ case PRIMARY_ACTION_TYPE_LARGE_ICON:
+ startMarginResId = R.dimen.car_keyline_4;
+ break;
+ default:
+ throw new IllegalStateException("Unrecognizable primary action type.");
+ }
+ int startMargin = mContext.getResources().getDimensionPixelSize(startMarginResId);
+ mBinders.add(vh -> {
+ RelativeLayout.LayoutParams titleLayoutParams =
+ (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams();
+ titleLayoutParams.setMarginStart(startMargin);
+ vh.getTitle().setLayoutParams(titleLayoutParams);
+
+ RelativeLayout.LayoutParams bodyLayoutParams =
+ (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams();
+ bodyLayoutParams.setMarginStart(startMargin);
+ vh.getBody().setLayoutParams(bodyLayoutParams);
+ });
+ }
+
+ /**
+ * Sets top/bottom margins of {@code Title} and {@code Body}.
+ */
+ private void setTextVerticalMargin() {
+ // Set all relevant fields in layout params to avoid carried over params when the item
+ // gets bound to a recycled view holder.
+ if (!TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mBody)) {
+ // Title only - view is aligned center vertically by itself.
+ mBinders.add(vh -> {
+ RelativeLayout.LayoutParams layoutParams =
+ (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams();
+ layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+ layoutParams.topMargin = 0;
+ vh.getTitle().setLayoutParams(layoutParams);
+ });
+ } else if (TextUtils.isEmpty(mTitle) && !TextUtils.isEmpty(mBody)) {
+ mBinders.add(vh -> {
+ // Body uses top and bottom margin.
+ int margin = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_padding_3);
+ RelativeLayout.LayoutParams layoutParams =
+ (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams();
+ layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+ layoutParams.removeRule(RelativeLayout.BELOW);
+ layoutParams.topMargin = margin;
+ layoutParams.bottomMargin = margin;
+ vh.getBody().setLayoutParams(layoutParams);
+ });
+ } else {
+ mBinders.add(vh -> {
+ // Title has a top margin
+ Resources resources = mContext.getResources();
+ int padding1 = resources.getDimensionPixelSize(R.dimen.car_padding_1);
+ int padding3 = resources.getDimensionPixelSize(R.dimen.car_padding_3);
+
+ RelativeLayout.LayoutParams titleLayoutParams =
+ (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams();
+ titleLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL);
+ titleLayoutParams.topMargin = padding3;
+ vh.getTitle().setLayoutParams(titleLayoutParams);
+ // Body is below title with a margin, and has bottom margin.
+ RelativeLayout.LayoutParams bodyLayoutParams =
+ (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams();
+ bodyLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL);
+ bodyLayoutParams.addRule(RelativeLayout.BELOW, R.id.title);
+ bodyLayoutParams.topMargin = padding1;
+ bodyLayoutParams.bottomMargin = padding3;
+ vh.getBody().setLayoutParams(bodyLayoutParams);
+ });
+ }
+ }
+
+ /**
+ * Sets up view(s) for supplemental action.
+ */
+ private void setSupplementalActions() {
+ switch (mSupplementalActionType) {
+ case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON:
+ mBinders.add((vh) -> {
+ vh.getSupplementalIcon().setVisibility(View.VISIBLE);
+ if (mShowSupplementalIconDivider) {
+ vh.getSupplementalIconDivider().setVisibility(View.VISIBLE);
+ }
+
+ vh.getSupplementalIcon().setImageResource(mSupplementalIconResId);
+ vh.getSupplementalIcon().setOnClickListener(
+ mSupplementalIconOnClickListener);
+ vh.getSupplementalIcon().setClickable(
+ mSupplementalIconOnClickListener != null);
+ });
+ break;
+ case SUPPLEMENTAL_ACTION_TWO_ACTIONS:
+ mBinders.add((vh) -> {
+ vh.getAction2().setVisibility(View.VISIBLE);
+ if (mShowAction2Divider) {
+ vh.getAction2Divider().setVisibility(View.VISIBLE);
+ }
+
+ vh.getAction2().setText(mAction2Text);
+ vh.getAction2().setOnClickListener(mAction2OnClickListener);
+ });
+ // Fall through
+ case SUPPLEMENTAL_ACTION_ONE_ACTION:
+ mBinders.add((vh) -> {
+ vh.getAction1().setVisibility(View.VISIBLE);
+ if (mShowAction1Divider) {
+ vh.getAction1Divider().setVisibility(View.VISIBLE);
+ }
+
+ vh.getAction1().setText(mAction1Text);
+ vh.getAction1().setOnClickListener(mAction1OnClickListener);
+ });
+ break;
+ case SUPPLEMENTAL_ACTION_NO_ACTION:
+ // Do nothing
+ break;
+ default:
+ throw new IllegalArgumentException("Unrecognized supplemental action type.");
+ }
+ }
+
+ /**
+ * Sets {@link View.OnClickListener} of {@code ListItem}.
+ *
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withOnClickListener(View.OnClickListener listener) {
+ mOnClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Sets {@code Primary Action} to be represented by an icon.
+ *
+ * @param iconResId the resource identifier of the drawable.
+ * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item
+ * with only title set; useful for album cover art.
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withPrimaryActionIcon(@DrawableRes int iconResId, boolean useLargeIcon) {
+ return withPrimaryActionIcon(null, iconResId, useLargeIcon);
+ }
+
+ /**
+ * Sets {@code Primary Action} to be represented by an icon.
+ *
+ * @param drawable the Drawable to set, or null to clear the content.
+ * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item
+ * with only title set; useful for album cover art.
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withPrimaryActionIcon(Drawable drawable, boolean useLargeIcon) {
+ return withPrimaryActionIcon(drawable, 0, useLargeIcon);
+ }
+
+ private Builder withPrimaryActionIcon(Drawable drawable, @DrawableRes int iconResId,
+ boolean useLargeIcon) {
+ mPrimaryActionType = useLargeIcon
+ ? PRIMARY_ACTION_TYPE_LARGE_ICON
+ : PRIMARY_ACTION_TYPE_SMALL_ICON;
+ mPrimaryActionIconResId = iconResId;
+ mPrimaryActionIconDrawable = drawable;
+ return this;
+ }
+
+ /**
+ * Sets {@code Primary Action} to be empty icon.
+ *
+ * {@code Text} would have a start margin as if {@code Primary Action} were set to
+ * primary icon.
+ *
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withPrimaryActionEmptyIcon() {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON;
+ return this;
+ }
+
+ /**
+ * Sets {@code Primary Action} to have no icon. Text would align to the start of item.
+ *
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withPrimaryActionNoIcon() {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
+ return this;
+ }
+
+ /**
+ * Sets the title of item.
+ *
+ * <p>Primary text is {@code title} by default. It can be set by
+ * {@link #withBody(String, boolean)}
+ *
+ * @param title text to display as title.
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * Sets the body text of item.
+ *
+ * <p>Text beyond length required by regulation will be truncated. Defaults {@code Title}
+ * text as the primary.
+ *
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withBody(String body) {
+ return withBody(body, false);
+ }
+
+ /**
+ * Sets the body text of item.
+ *
+ * <p>Text beyond length required by regulation will be truncated.
+ *
+ * @param asPrimary sets {@code Body Text} as primary text of item.
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withBody(String body, boolean asPrimary) {
+ int limit = mContext.getResources().getInteger(
+ R.integer.car_list_item_text_length_limit);
+ if (body.length() < limit) {
+ mBody = body;
+ } else {
+ mBody = body.substring(0, limit) + mContext.getString(R.string.ellipsis);
+ }
+ mIsBodyPrimary = asPrimary;
+ return this;
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ *
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withSupplementalIcon(int iconResId, boolean showDivider) {
+ return withSupplementalIcon(iconResId, showDivider, null);
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ *
+ * @param iconResId drawable resource id.
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withSupplementalIcon(int iconResId, boolean showDivider,
+ View.OnClickListener listener) {
+ mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON;
+
+ mSupplementalIconResId = iconResId;
+ mSupplementalIconOnClickListener = listener;
+ mShowSupplementalIconDivider = showDivider;
+ return this;
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Action Button}.
+ *
+ * @param text button text to display.
+ * @return This Builder object to allow for chaining calls to set methods.
+ */
+ public Builder withAction(String text, boolean showDivider, View.OnClickListener listener) {
+ if (TextUtils.isEmpty(text)) {
+ throw new IllegalArgumentException("Action text cannot be empty.");
+ }
+ if (listener == null) {
+ throw new IllegalArgumentException("Action OnClickListener cannot be null.");
+ }
+ mSupplementalActionType = SUPPLEMENTAL_ACTION_ONE_ACTION;
+
+ mAction1Text = text;
+ mAction1OnClickListener = listener;
+ mShowAction1Divider = showDivider;
+ return this;
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by two {@code Action Button}s.
+ *
+ * <p>These two action buttons will be aligned towards item end.
+ *
+ * @param action1Text button text to display - this button will be closer to item end.
+ * @param action2Text button text to display.
+ */
+ public Builder withActions(String action1Text, boolean showAction1Divider,
+ View.OnClickListener action1OnClickListener,
+ String action2Text, boolean showAction2Divider,
+ View.OnClickListener action2OnClickListener) {
+ if (TextUtils.isEmpty(action1Text) || TextUtils.isEmpty(action2Text)) {
+ throw new IllegalArgumentException("Action text cannot be empty.");
+ }
+ if (action1OnClickListener == null || action2OnClickListener == null) {
+ throw new IllegalArgumentException("Action OnClickListener cannot be null.");
+ }
+ mSupplementalActionType = SUPPLEMENTAL_ACTION_TWO_ACTIONS;
+
+ mAction1Text = action1Text;
+ mAction1OnClickListener = action1OnClickListener;
+ mShowAction1Divider = showAction1Divider;
+ mAction2Text = action2Text;
+ mAction2OnClickListener = action2OnClickListener;
+ mShowAction2Divider = showAction2Divider;
+ return this;
+ }
+
+ /**
+ * Adds {@link ViewBinder} to interact with sub-views in
+ * {@link ListItemAdapter.ViewHolder}. These ViewBinders will always bind after
+ * other {@link Builder} methods have bond.
+ *
+ * <p>Make sure to call with...() method on the intended sub-view first.
+ *
+ * <p>Example:
+ * <pre>
+ * {@code
+ * new Builder()
+ * .withTitle("title")
+ * .withViewBinder((viewHolder) -> {
+ * viewHolder.getTitle().doMoreStuff();
+ * })
+ * .build();
+ * }
+ * </pre>
+ */
+ public Builder withViewBinder(ViewBinder binder) {
+ mCustomBinders.add(binder);
+ return this;
+ }
+ }
+}
diff --git a/androidx/car/widget/ListItemAdapter.java b/androidx/car/widget/ListItemAdapter.java
new file mode 100644
index 00000000..c9b61773
--- /dev/null
+++ b/androidx/car/widget/ListItemAdapter.java
@@ -0,0 +1,228 @@
+/*
+ * 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 androidx.car.widget;
+
+import android.content.Context;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.car.R;
+import androidx.car.utils.ListItemBackgroundResolver;
+
+/**
+ * Adapter for {@link PagedListView} to display {@link ListItem}.
+ *
+ * Implements {@link PagedListView.ItemCap} - defaults to unlimited item count.
+ */
+public class ListItemAdapter extends
+ RecyclerView.Adapter<ListItemAdapter.ViewHolder> implements PagedListView.ItemCap {
+
+ /**
+ * Constant class for background style of items.
+ */
+ public static final class BackgroundStyle {
+ private BackgroundStyle() {}
+
+ /**
+ * Sets the background color of each item.
+ */
+ public static final int NONE = 0;
+ /**
+ * Sets each item in {@link CardView} with a rounded corner background and shadow.
+ */
+ public static final int CARD = 1;
+ /**
+ * Sets background of each item so the combined list looks like one elongated card, namely
+ * top and bottom item will have rounded corner at only top/bottom side respectively. If
+ * only one item exists, it will have both top and bottom rounded corner.
+ */
+ public static final int PANEL = 2;
+ }
+
+ private int mBackgroundStyle;
+
+ private final Context mContext;
+ private final ListItemProvider mItemProvider;
+
+ private int mMaxItems = PagedListView.ItemCap.UNLIMITED;
+
+ public ListItemAdapter(Context context, ListItemProvider itemProvider) {
+ this(context, itemProvider, BackgroundStyle.NONE);
+ }
+
+ public ListItemAdapter(Context context, ListItemProvider itemProvider,
+ int backgroundStyle) {
+ mContext = context;
+ mItemProvider = itemProvider;
+ mBackgroundStyle = backgroundStyle;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ View itemView = inflater.inflate(R.layout.car_paged_list_item_content, parent, false);
+
+ ViewGroup container = createListItemContainer();
+ container.addView(itemView);
+ return new ViewHolder(container);
+ }
+
+ private ViewGroup createListItemContainer() {
+ ViewGroup container;
+ switch (mBackgroundStyle) {
+ case BackgroundStyle.NONE:
+ case BackgroundStyle.PANEL:
+ FrameLayout frameLayout = new FrameLayout(mContext);
+ frameLayout.setLayoutParams(new RecyclerView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ frameLayout.setBackgroundColor(mContext.getColor(R.color.car_card));
+
+ container = frameLayout;
+ break;
+ case BackgroundStyle.CARD:
+ CardView card = new CardView(mContext);
+ RecyclerView.LayoutParams cardLayoutParams = new RecyclerView.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ cardLayoutParams.bottomMargin = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_padding_3);
+ card.setLayoutParams(cardLayoutParams);
+ card.setRadius(mContext.getResources().getDimensionPixelSize(R.dimen.car_radius_1));
+ card.setCardBackgroundColor(mContext.getColor(R.color.car_card));
+
+ container = card;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown background style. "
+ + "Expected constants in class ListItemAdapter.BackgroundStyle.");
+ }
+ return container;
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ ListItem item = mItemProvider.get(position);
+ item.bind(holder);
+
+ if (mBackgroundStyle == BackgroundStyle.PANEL) {
+ ListItemBackgroundResolver.setBackground(
+ holder.itemView, position, mItemProvider.size());
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mMaxItems == PagedListView.ItemCap.UNLIMITED
+ ? mItemProvider.size()
+ : Math.min(mItemProvider.size(), mMaxItems);
+ }
+
+ @Override
+ public void setMaxItems(int maxItems) {
+ mMaxItems = maxItems;
+ }
+
+ /**
+ * Holds views of an item in PagedListView.
+ *
+ * <p>This ViewHolder maps to views in layout car_paged_list_item_content.xml.
+ */
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+
+ private RelativeLayout mContainerLayout;
+
+ private ImageView mPrimaryIcon;
+
+ private TextView mTitle;
+ private TextView mBody;
+
+ private View mSupplementalIconDivider;
+ private ImageView mSupplementalIcon;
+
+ private Button mAction1;
+ private View mAction1Divider;
+
+ private Button mAction2;
+ private View mAction2Divider;
+
+ public ViewHolder(View itemView) {
+ super(itemView);
+
+ mContainerLayout = itemView.findViewById(R.id.container);
+
+ mPrimaryIcon = itemView.findViewById(R.id.primary_icon);
+
+ mTitle = itemView.findViewById(R.id.title);
+ mBody = itemView.findViewById(R.id.body);
+
+ mSupplementalIcon = itemView.findViewById(R.id.supplemental_icon);
+ mSupplementalIconDivider = itemView.findViewById(R.id.supplemental_icon_divider);
+
+ mAction1 = itemView.findViewById(R.id.action1);
+ mAction1Divider = itemView.findViewById(R.id.action1_divider);
+ mAction2 = itemView.findViewById(R.id.action2);
+ mAction2Divider = itemView.findViewById(R.id.action2_divider);
+ }
+
+ public RelativeLayout getContainerLayout() {
+ return mContainerLayout;
+ }
+
+ public ImageView getPrimaryIcon() {
+ return mPrimaryIcon;
+ }
+
+ public TextView getTitle() {
+ return mTitle;
+ }
+
+ public TextView getBody() {
+ return mBody;
+ }
+
+ public ImageView getSupplementalIcon() {
+ return mSupplementalIcon;
+ }
+
+ public View getSupplementalIconDivider() {
+ return mSupplementalIconDivider;
+ }
+
+ public Button getAction1() {
+ return mAction1;
+ }
+
+ public View getAction1Divider() {
+ return mAction1Divider;
+ }
+
+ public Button getAction2() {
+ return mAction2;
+ }
+
+ public View getAction2Divider() {
+ return mAction2Divider;
+ }
+ }
+}
diff --git a/androidx/car/widget/ListItemProvider.java b/androidx/car/widget/ListItemProvider.java
new file mode 100644
index 00000000..5a3edbd7
--- /dev/null
+++ b/androidx/car/widget/ListItemProvider.java
@@ -0,0 +1,56 @@
+/*
+ * 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 androidx.car.widget;
+
+import java.util.List;
+
+/**
+ * Supplies data as {@link ListItem}.
+ */
+public abstract class ListItemProvider {
+
+ /**
+ * Returns {@link ListItem} at requested position.
+ */
+ public abstract ListItem get(int position);
+
+ /**
+ * @return number of total items.
+ */
+ public abstract int size();
+
+ /**
+ * A simple provider that wraps around a list.
+ */
+ public static class ListProvider extends ListItemProvider {
+ private final List<ListItem> mItems;
+
+ public ListProvider(List<ListItem> items) {
+ mItems = items;
+ }
+
+ @Override
+ public ListItem get(int position) {
+ return mItems.get(position);
+ }
+
+ @Override
+ public int size() {
+ return mItems.size();
+ }
+ }
+}
diff --git a/android/support/car/widget/PagedListView.java b/androidx/car/widget/PagedListView.java
index 67a6247a..d90d670b 100644
--- a/android/support/car/widget/PagedListView.java
+++ b/androidx/car/widget/PagedListView.java
@@ -14,9 +14,7 @@
* limitations under the License.
*/
-package android.support.car.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+package androidx.car.widget;
import android.content.Context;
import android.content.res.Resources;
@@ -25,30 +23,49 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.os.Bundle;
import android.os.Handler;
-import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.ColorRes;
import android.support.annotation.IdRes;
+import android.support.annotation.IntDef;
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.annotation.VisibleForTesting;
+import android.support.v7.widget.LinearLayoutManager;
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;
+import androidx.car.R;
+
/**
- * 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.
+ * View that wraps a {@link android.support.v7.widget.RecyclerView} and a scroll bar that has
+ * page up and down arrows. Interaction with this view is similar to a {@code RecyclerView} as it
+ * takes the same adapter.
+ *
+ * <p>By default, this PagedListView utilizes a vertical {@link LinearLayoutManager} to display
+ * its items.
*/
public class PagedListView extends FrameLayout {
+ /**
+ * The key used to save the state of this PagedListView's super class in
+ * {@link #onSaveInstanceState()}.
+ */
+ private static final String SAVED_SUPER_STATE_KEY = "PagedListViewSuperState";
+
+ /**
+ * The key used to save the state of {@link #mRecyclerView} so that it can be restored
+ * on configuration change. The actual saving of state will be controlled by the LayoutManager
+ * of the RecyclerView; this value simply ensures the state is passed on to the LayoutManager.
+ */
+ private static final String SAVED_RECYCLER_VIEW_STATE_KEY = "RecyclerViewState";
+
/** Default maximum number of clicks allowed on a list */
public static final int DEFAULT_MAX_CLICKS = 6;
@@ -62,30 +79,47 @@ public class PagedListView extends FrameLayout {
* 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 int PAGINATION_HOLD_DELAY_MS = 400;
+
+ /**
+ * A fling distance to use when the up button is pressed. This value is arbitrary and just needs
+ * to be large enough so that the maximum amount of fling is applied. The
+ * {@link PagedSnapHelper} will handle capping this value so that the RecyclerView is scrolled
+ * one page upwards.
+ */
+ private static final int FLING_UP_DISTANCE = -10000;
+
+ /**
+ * A fling distance to use when the down button is pressed. This value is arbitrary and just
+ * needs to be large enough so that the maximum amount of fling is applied. The
+ * {@link PagedSnapHelper} will handle capping this value so that the RecyclerView is scrolled
+ * one page downwards.
+ */
+ private static final int FLING_DOWN_DISTANCE = 10000;
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 RecyclerView mRecyclerView;
+ private final PagedSnapHelper mSnapHelper;
+ private final Handler mHandler = new Handler();
private final boolean mScrollBarEnabled;
- private final PagedScrollBarView mScrollBarView;
+ @VisibleForTesting
+ final PagedScrollBarView mScrollBarView;
private int mRowsPerPage = -1;
- protected RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter;
+ private RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter;
/** Maximum number of pages to show. */
private int mMaxPages;
- protected OnScrollListener mOnScrollListener;
+ private 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 int mLastItemCount;
private boolean mNeedsFocus;
@@ -119,6 +153,39 @@ public class PagedListView extends FrameLayout {
}
/**
+ * The possible values for @{link #setGutter}. The default value is actually
+ * {@link Gutter#BOTH}.
+ */
+ @IntDef({
+ Gutter.NONE,
+ Gutter.START,
+ Gutter.END,
+ Gutter.BOTH,
+ })
+ public @interface Gutter {
+ /**
+ * No gutter on either side of the list items. The items will span the full width of the
+ * {@link PagedListView}.
+ */
+ int NONE = 0;
+
+ /**
+ * Include a gutter only on the start side (that is, the same side as the scroll bar).
+ */
+ int START = 1;
+
+ /**
+ * Include a gutter only on the end side (that is, the opposite side of the scroll bar).
+ */
+ int END = 2;
+
+ /**
+ * Include a gutter on both sides of the list items. This is the default behaviour.
+ */
+ int BOTH = 3;
+ }
+
+ /**
* Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to set the position
* offset for the adapter to load the data.
*
@@ -153,28 +220,31 @@ public class PagedListView extends FrameLayout {
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);
+ mRecyclerView = findViewById(R.id.recycler_view);
mMaxPages = getDefaultMaxPages();
- mLayoutManager = new PagedLayoutManager(context);
- mLayoutManager.setOffsetRows(offsetRows);
- mRecyclerView.setLayoutManager(mLayoutManager);
+ RecyclerView.LayoutManager layoutManager =
+ new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
+ mRecyclerView.setLayoutManager(layoutManager);
+
+ mSnapHelper = new PagedSnapHelper();
+ mSnapHelper.attachToRecyclerView(mRecyclerView);
+
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.hasValue(R.styleable.PagedListView_gutter)) {
+ int gutter = a.getInt(R.styleable.PagedListView_gutter, Gutter.BOTH);
+ setGutter(gutter);
+ } else if (a.hasValue(R.styleable.PagedListView_offsetScrollBar)) {
+ boolean offsetScrollBar =
+ a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false);
+ if (offsetScrollBar) {
+ setGutter(Gutter.START);
+ }
+ } else {
+ setGutter(Gutter.BOTH);
}
if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) {
@@ -206,25 +276,24 @@ public class PagedListView extends FrameLayout {
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 + ")");
- }
+ mScrollBarView.setPaginationListener(direction -> {
+ switch (direction) {
+ case PagedScrollBarView.PaginationListener.PAGE_UP:
+ pageUp();
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollUpButtonClicked();
+ }
+ break;
+ case PagedScrollBarView.PaginationListener.PAGE_DOWN:
+ pageDown();
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollDownButtonClicked();
}
- });
+ break;
+ default:
+ Log.e(TAG, "Unknown pagination direction (" + direction + ")");
+ }
+ });
Drawable upButtonIcon = a.getDrawable(R.styleable.PagedListView_upButtonIcon);
if (upButtonIcon != null) {
@@ -255,24 +324,6 @@ public class PagedListView extends FrameLayout {
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.
*
@@ -280,14 +331,40 @@ public class PagedListView extends FrameLayout {
* @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) {
+ if (v == null || v.getParent() != mRecyclerView
+ || mRecyclerView.getLayoutManager() == null) {
return -1;
}
- return mLayoutManager.getPosition(v);
+ return mRecyclerView.getLayoutManager().getPosition(v);
+ }
+
+ /**
+ * Set the gutter to the specified value.
+ *
+ * The gutter is the space to the start/end of the list view items and will be equal in size
+ * to the scroll bars. By default, there is a gutter to both the left and right of the list
+ * view items, to account for the scroll bar.
+ *
+ * @param gutter A {@link Gutter} value that identifies which sides to apply the gutter to.
+ */
+ public void setGutter(@Gutter int gutter) {
+ int startPadding = 0;
+ int endPadding = 0;
+ if ((gutter & Gutter.START) != 0) {
+ startPadding = getResources().getDimensionPixelSize(R.dimen.car_margin);
+ }
+ if ((gutter & Gutter.END) != 0) {
+ endPadding = getResources().getDimensionPixelSize(R.dimen.car_margin);
+ }
+ mRecyclerView.setPaddingRelative(startPadding, 0, endPadding, 0);
+
+ // If there's a gutter, set ClipToPadding to false so that CardView's shadow will still
+ // appear outside of the padding.
+ mRecyclerView.setClipToPadding(startPadding == 0 && endPadding == 0);
}
@NonNull
- public CarRecyclerView getRecyclerView() {
+ public RecyclerView getRecyclerView() {
return mRecyclerView;
}
@@ -297,10 +374,17 @@ public class PagedListView extends FrameLayout {
* @param position The position in the list to scroll to.
*/
public void scrollToPosition(int position) {
- mLayoutManager.scrollToPosition(position);
+ if (mRecyclerView.getLayoutManager() == null) {
+ return;
+ }
+
+ PagedSmoothScroller smoothScroller = new PagedSmoothScroller(getContext());
+ smoothScroller.setTargetPosition(position);
+
+ mRecyclerView.getLayoutManager().startSmoothScroll(smoothScroller);
// 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
+ // the pagination arrows actually get updated. See b/15801119
mHandler.post(mUpdatePaginationRunnable);
}
@@ -328,13 +412,6 @@ public class PagedListView extends FrameLayout {
updateMaxItems();
}
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @NonNull
- public PagedLayoutManager getLayoutManager() {
- return mLayoutManager;
- }
-
@Nullable
@SuppressWarnings("unchecked")
public RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() {
@@ -386,22 +463,6 @@ public class PagedListView extends FrameLayout {
}
/**
- * @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.
@@ -451,6 +512,25 @@ public class PagedListView extends FrameLayout {
}
/**
+ * Sets the color of scrollbar.
+ *
+ * <p>Custom color ignores {@link DayNightStyle}. Calling {@link #resetScrollbarColor} resets to
+ * default color.
+ *
+ * @param color Resource identifier of the color.
+ */
+ public void setScrollbarColor(@ColorRes int color) {
+ mScrollBarView.setThumbColor(color);
+ }
+
+ /**
+ * Resets the color of scrollbar to default.
+ */
+ public void resetScrollbarColor() {
+ mScrollBarView.resetThumbColor();
+ }
+
+ /**
* Adds an {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} to this
* PagedListView.
*
@@ -473,6 +553,7 @@ public class PagedListView extends FrameLayout {
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.
@@ -494,30 +575,6 @@ public class PagedListView extends FrameLayout {
}
/**
- * 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.
*
@@ -525,7 +582,6 @@ public class PagedListView extends FrameLayout {
*/
public void setOnScrollListener(OnScrollListener listener) {
mOnScrollListener = listener;
- mLayoutManager.setOnScrollListener(mOnScrollListener);
}
/** Returns the page the given position is on, starting with page 0. */
@@ -539,6 +595,16 @@ public class PagedListView extends FrameLayout {
return position / mRowsPerPage;
}
+ /** Scrolls the contents of the RecyclerView up a page. */
+ private void pageUp() {
+ mRecyclerView.fling(0, FLING_UP_DISTANCE);
+ }
+
+ /** Scrolls the contents of the RecyclerView down a page. */
+ private void pageDown() {
+ mRecyclerView.fling(0, FLING_DOWN_DISTANCE);
+ }
+
/**
* Sets the default number of pages that this PagedListView is limited to.
*
@@ -553,7 +619,7 @@ public class PagedListView extends FrameLayout {
}
/** Returns the default number of pages the list should have */
- protected int getDefaultMaxPages() {
+ private int getDefaultMaxPages() {
// assume list shown in response to a click, so, reduce number of clicks by one
return mDefaultMaxPages - 1;
}
@@ -571,11 +637,16 @@ public class PagedListView extends FrameLayout {
// 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.
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+
+ if (layoutManager == null) {
+ return;
+ }
// 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);
+ View focusedChild = layoutManager.getFocusedChild();
+ View firstBorn = layoutManager.getChildAt(0);
super.onLayout(changed, left, top, right, bottom);
@@ -611,6 +682,7 @@ public class PagedListView extends FrameLayout {
}
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.
@@ -619,17 +691,6 @@ public class PagedListView extends FrameLayout {
}
/**
- * 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
@@ -639,39 +700,55 @@ public class PagedListView extends FrameLayout {
* @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) {
+ private 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) {
+ boolean isAtStart = isAtStart();
+ boolean isAtEnd = isAtEnd();
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+
+ if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
mScrollBarView.setVisibility(View.INVISIBLE);
} else {
mScrollBarView.setVisibility(View.VISIBLE);
}
- mScrollBarView.setUpEnabled(shouldEnablePageUpButton());
- mScrollBarView.setDownEnabled(shouldEnablePageDownButton());
+ mScrollBarView.setUpEnabled(!isAtStart);
+ mScrollBarView.setDownEnabled(!isAtEnd);
+
+ if (layoutManager == null) {
+ return;
+ }
+
+ if (mRecyclerView.getLayoutManager().canScrollVertically()) {
+ mScrollBarView.setParameters(
+ mRecyclerView.computeVerticalScrollRange(),
+ mRecyclerView.computeVerticalScrollOffset(),
+ mRecyclerView.computeVerticalScrollExtent(), animate);
+ } else {
+ mScrollBarView.setParameters(
+ mRecyclerView.computeHorizontalScrollRange(),
+ mRecyclerView.computeHorizontalScrollOffset(),
+ mRecyclerView.computeHorizontalScrollExtent(), animate);
+ }
- mScrollBarView.setParameters(
- mRecyclerView.computeVerticalScrollRange(),
- mRecyclerView.computeVerticalScrollOffset(),
- mRecyclerView.computeVerticalScrollExtent(),
- animate);
invalidate();
}
- protected boolean shouldEnablePageUpButton() {
- return !mLayoutManager.isAtTop();
+ /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
+ public boolean isAtStart() {
+ return mSnapHelper.isAtStart(mRecyclerView.getLayoutManager());
}
- protected boolean shouldEnablePageDownButton() {
- return !mLayoutManager.isAtBottom();
+ /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
+ public boolean isAtEnd() {
+ return mSnapHelper.isAtEnd(mRecyclerView.getLayoutManager());
}
@UiThread
- protected void updateMaxItems() {
+ private void updateMaxItems() {
if (mAdapter == null) {
return;
}
@@ -698,8 +775,13 @@ public class PagedListView extends FrameLayout {
}
}
- protected int calculateMaxItemCount() {
- final View firstChild = mLayoutManager.getChildAt(0);
+ private int calculateMaxItemCount() {
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+ if (layoutManager == null) {
+ return -1;
+ }
+
+ View firstChild = layoutManager.getChildAt(0);
if (firstChild == null || firstChild.getHeight() == 0) {
return -1;
} else {
@@ -711,8 +793,14 @@ public class PagedListView extends FrameLayout {
* 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);
+ private void updateRowsPerPage() {
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+ if (layoutManager == null) {
+ mRowsPerPage = 1;
+ return;
+ }
+
+ View firstChild = layoutManager.getChildAt(0);
if (firstChild == null || firstChild.getHeight() == 0) {
mRowsPerPage = 1;
} else {
@@ -721,24 +809,36 @@ public class PagedListView extends FrameLayout {
}
@Override
- protected Parcelable onSaveInstanceState() {
- SavedState savedState = new SavedState(super.onSaveInstanceState());
- savedState.mLayoutManagerState = mLayoutManager.onSaveInstanceState();
- return savedState;
+ public Parcelable onSaveInstanceState() {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(SAVED_SUPER_STATE_KEY, super.onSaveInstanceState());
+
+ SparseArray<Parcelable> recyclerViewState = new SparseArray<>();
+ mRecyclerView.saveHierarchyState(recyclerViewState);
+ bundle.putSparseParcelableArray(SAVED_RECYCLER_VIEW_STATE_KEY, recyclerViewState);
+
+ return bundle;
}
@Override
- protected void onRestoreInstanceState(Parcelable state) {
- SavedState savedState = (SavedState) state;
- mLayoutManager.onRestoreInstanceState(savedState.mLayoutManagerState);
- super.onRestoreInstanceState(savedState.getSuperState());
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof Bundle)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ Bundle bundle = (Bundle) state;
+ mRecyclerView.restoreHierarchyState(
+ bundle.getSparseParcelableArray(SAVED_RECYCLER_VIEW_STATE_KEY));
+
+ super.onRestoreInstanceState(bundle.getParcelable(SAVED_SUPER_STATE_KEY));
}
@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
+ // cannot be used as is. 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);
@@ -752,56 +852,15 @@ public class PagedListView extends FrameLayout {
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()) {
+
+ if (!isAtStart() && isAtEnd()) {
mOnScrollListener.onReachBottom();
- } else if (mLayoutManager.isAtTop() || !mLayoutManager.isAtBottom()) {
- mOnScrollListener.onLeaveBottom();
}
}
updatePaginationButtons(false);
@@ -818,7 +877,7 @@ public class PagedListView extends FrameLayout {
}
};
- protected final Runnable mPaginationRunnable =
+ private final Runnable mPaginationRunnable =
new Runnable() {
@Override
public void run() {
@@ -828,35 +887,24 @@ public class PagedListView extends FrameLayout {
return;
}
if (upPressed) {
- mRecyclerView.pageUp();
+ pageUp();
} else if (downPressed) {
- mRecyclerView.pageDown();
+ pageDown();
}
}
};
private final Runnable mUpdatePaginationRunnable =
- new Runnable() {
- @Override
- public void run() {
- updatePaginationButtons(true /*animate*/);
- }
- };
+ () -> 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
@@ -866,12 +914,6 @@ public class PagedListView extends FrameLayout {
/** 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() {}
}
/**
@@ -879,32 +921,31 @@ public class PagedListView extends FrameLayout {
* between each item in the RecyclerView that it is added to.
*/
private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
-
- private int mHalfItemSpacing;
+ private int mItemSpacing;
private ItemSpacingDecoration(int itemSpacing) {
- mHalfItemSpacing = itemSpacing / 2;
+ mItemSpacing = itemSpacing;
}
@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;
+
+ // Skip offset for last item.
+ if (position == state.getItemCount() - 1) {
+ return;
}
+
+ outRect.bottom = mItemSpacing;
}
/**
* @param itemSpacing sets spacing between each item.
*/
public void setItemSpacing(int itemSpacing) {
- mHalfItemSpacing = itemSpacing / 2;
+ mItemSpacing = itemSpacing;
}
}
@@ -940,7 +981,7 @@ public class PagedListView extends FrameLayout {
Resources res = context.getResources();
mPaint = new Paint();
mPaint.setColor(res.getColor(R.color.car_list_divider));
- mDividerHeight = res.getDimensionPixelSize(R.dimen.car_divider_height);
+ mDividerHeight = res.getDimensionPixelSize(R.dimen.car_list_divider_height);
}
/** Updates the list divider color which may have changed due to a day night transition. */
@@ -970,8 +1011,18 @@ public class PagedListView extends FrameLayout {
continue;
}
- int left = mDividerStartMargin + startChild.getLeft();
- int right = endChild.getRight();
+ Rect containerRect = new Rect();
+ container.getGlobalVisibleRect(containerRect);
+
+ Rect startRect = new Rect();
+ startChild.getGlobalVisibleRect(startRect);
+
+ Rect endRect = new Rect();
+ endChild.getGlobalVisibleRect(endRect);
+
+ int left = container.getLeft() + mDividerStartMargin
+ + (startRect.left - containerRect.left);
+ int right = container.getRight() - (endRect.right - containerRect.right);
int bottom = container.getBottom() + spacing / 2 + mDividerHeight / 2;
int top = bottom - mDividerHeight;
diff --git a/android/support/car/widget/PagedScrollBarView.java b/androidx/car/widget/PagedScrollBarView.java
index 1c46b5d4..c9a392e6 100644
--- a/android/support/car/widget/PagedScrollBarView.java
+++ b/androidx/car/widget/PagedScrollBarView.java
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-package android.support.car.widget;
+package androidx.car.widget;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
-import android.support.car.R;
+import android.support.annotation.ColorRes;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.LayoutInflater;
@@ -30,6 +31,8 @@ import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
+import androidx.car.R;
+
/** A custom view to provide list scroll behaviour -- up/down buttons and scroll indicator. */
public class PagedScrollBarView extends FrameLayout
implements View.OnClickListener, View.OnLongClickListener {
@@ -48,13 +51,14 @@ public class PagedScrollBarView extends FrameLayout
private final ImageView mUpButton;
private final ImageView mDownButton;
- private final ImageView mScrollThumb;
+ @VisibleForTesting
+ 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 boolean mUseCustomThumbBackground;
+ @ColorRes private int mCustomThumbBackgroundResId;
private PaginationListener mPaginationListener;
public PagedScrollBarView(Context context, AttributeSet attrs) {
@@ -83,9 +87,6 @@ public class PagedScrollBarView extends FrameLayout
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
@@ -128,25 +129,44 @@ public class PagedScrollBarView extends FrameLayout
return mDownButton.isPressed();
}
- /** Sets the range, offset and extent of the scroll bar. See {@link View}. */
+ /**
+ * Sets the range, offset and extent of the scroll bar. The range represents the size of a
+ * container for the scrollbar thumb; offset is the distance from the start of the container
+ * to where the thumb should be; and finally, extent is the size of the thumb.
+ *
+ * <p>These values can be expressed in arbitrary units, so long as they share the same units.
+ *
+ * @param range The range of the scrollbar's thumb
+ * @param offset The offset of the scrollbar's thumb
+ * @param extent The extent of the scrollbar's thumb
+ * @param animate Whether or not the thumb should animate from its current position to the
+ * position specified by the given range, offset and extent.
+ *
+ * @see View#computeVerticalScrollRange()
+ * @see View#computeVerticalScrollOffset()
+ * @see View#computeVerticalScrollExtent()
+ */
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;
+ // If the scroll bars aren't visible, then no need to update.
+ if (getVisibility() == View.GONE || range == 0) {
+ return;
}
+ int availableSpace = mFiller.getHeight() - mFiller.getPaddingTop()
+ - mFiller.getPaddingBottom();
+
+ // Scale the length by the available space that the thumb can fill.
+ int thumbLength = Math.round(((float) extent / range) * availableSpace);
+
+ // Ensure that if the user has reached the bottom of the list, then the scroll bar is
+ // aligned to the bottom as well. Otherwise, scale the offset appropriately.
+ int thumbOffset = isDownEnabled()
+ ? Math.round(((float) offset / range) * availableSpace)
+ : availableSpace - thumbLength;
+
// Sets the size of the thumb and request a redraw if needed.
- final ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
+ ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
+
if (lp.height != thumbLength) {
lp.height = thumbLength;
mScrollThumb.requestLayout();
@@ -196,43 +216,65 @@ public class PagedScrollBarView extends FrameLayout
return mDownButton.isEnabled();
}
+ /**
+ * Sets the color of thumb.
+ *
+ * <p>Custom thumb color ignores {@link DayNightStyle}. Calling {@link #resetThumbColor} resets
+ * to default color.
+ *
+ * @param color Resource identifier of the color.
+ */
+ public void setThumbColor(@ColorRes int color) {
+ mUseCustomThumbBackground = true;
+ mCustomThumbBackgroundResId = color;
+ reloadColors();
+ }
+
+ /**
+ * Resets the color of thumb to default.
+ */
+ public void resetThumbColor() {
+ mUseCustomThumbBackground = false;
+ reloadColors();
+ }
+
/** Reload the colors for the current {@link DayNightStyle}. */
private void reloadColors() {
- int tint;
- int thumbBackground;
+ int tintResId;
+ int thumbBackgroundResId;
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;
+ tintResId = R.color.car_tint;
+ thumbBackgroundResId = R.color.car_scrollbar_thumb;
+ upDownBackgroundResId = R.drawable.car_card_ripple_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;
+ tintResId = R.color.car_tint_inverse;
+ thumbBackgroundResId = R.color.car_scrollbar_thumb_inverse;
+ upDownBackgroundResId = R.drawable.car_card_ripple_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;
+ tintResId = R.color.car_tint_light;
+ thumbBackgroundResId = R.color.car_scrollbar_thumb_light;
+ upDownBackgroundResId = R.drawable.car_card_ripple_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;
+ tintResId = R.color.car_tint_dark;
+ thumbBackgroundResId = R.color.car_scrollbar_thumb_dark;
+ upDownBackgroundResId = R.drawable.car_card_ripple_background_day;
break;
default:
throw new IllegalArgumentException("Unknown DayNightStyle: " + mDayNightStyle);
}
- mScrollThumb.setBackgroundColor(thumbBackground);
+ if (mUseCustomThumbBackground) {
+ thumbBackgroundResId = mCustomThumbBackgroundResId;
+ }
+ mScrollThumb.setBackgroundColor(ContextCompat.getColor(getContext(), thumbBackgroundResId));
+ int tint = ContextCompat.getColor(getContext(), tintResId);
mUpButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
mUpButton.setBackgroundResource(upDownBackgroundResId);
diff --git a/androidx/car/widget/PagedSmoothScroller.java b/androidx/car/widget/PagedSmoothScroller.java
new file mode 100644
index 00000000..cbd78621
--- /dev/null
+++ b/androidx/car/widget/PagedSmoothScroller.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 androidx.car.widget;
+
+import android.content.Context;
+import android.support.v7.widget.LinearSmoothScroller;
+import android.support.v7.widget.RecyclerView;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+/**
+ * Custom {@link LinearSmoothScroller} that has:
+ *
+ * <ul>
+ * <li>Custom control over the speed of scrolls.
+ * <li>Scrolling that snaps to start of a child view.
+ * </ul>
+ */
+public final class PagedSmoothScroller extends LinearSmoothScroller {
+ private static final float MILLISECONDS_PER_INCH = 150f;
+ private static final float DECELERATION_TIME_DIVISOR = 0.45f;
+
+ private final Interpolator mInterpolator = new DecelerateInterpolator(1.8f);
+
+ public PagedSmoothScroller(Context context) {
+ super(context);
+ }
+
+ @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 SNAP_TO_START;
+ }
+
+ @Override
+ protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+ int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START);
+ if (dy == 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);
+ }
+}
diff --git a/androidx/car/widget/PagedSnapHelper.java b/androidx/car/widget/PagedSnapHelper.java
new file mode 100644
index 00000000..ad1c7104
--- /dev/null
+++ b/androidx/car/widget/PagedSnapHelper.java
@@ -0,0 +1,269 @@
+/*
+ * 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 androidx.car.widget;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.LinearSnapHelper;
+import android.support.v7.widget.OrientationHelper;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/**
+ * Extension of a {@link LinearSnapHelper} that will snap to the start of the target child view to
+ * the start of the attached {@link RecyclerView}. The start of the view is defined as the top
+ * if the RecyclerView is scrolling vertically; it is defined as the left (or right if RTL) if the
+ * RecyclerView is scrolling horizontally.
+ */
+public class PagedSnapHelper extends LinearSnapHelper {
+ /**
+ * The percentage of a View that needs to be completely visible for it to be a viable snap
+ * target.
+ */
+ private static final float VIEW_VISIBLE_THRESHOLD = 0.5f;
+
+ private RecyclerView mRecyclerView;
+
+ // Orientation helpers are lazily created per LayoutManager.
+ @Nullable
+ private OrientationHelper mVerticalHelper;
+
+ @Nullable
+ private OrientationHelper mHorizontalHelper;
+
+ @Override
+ public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
+ @NonNull View targetView) {
+ int[] out = new int[2];
+
+ out[0] = layoutManager.canScrollHorizontally()
+ ? getHorizontalHelper(layoutManager).getDecoratedStart(targetView)
+ : 0;
+
+ out[1] = layoutManager.canScrollVertically()
+ ? getVerticalHelper(layoutManager).getDecoratedStart(targetView)
+ : 0;
+
+ return out;
+ }
+
+ /**
+ * Finds the view to snap to. The view to snap to is the child of the LayoutManager that is
+ * closest to the start of the RecyclerView. The "start" depends on if the LayoutManager
+ * is scrolling horizontally or vertically. If it is horizontally scrolling, then the
+ * start is the view on the left (right if RTL). Otherwise, it is the top-most view.
+ *
+ * @param layoutManager The current {@link RecyclerView.LayoutManager} for the attached
+ * RecyclerView.
+ * @return The View closest to the start of the RecyclerView.
+ */
+ @Override
+ @Nullable
+ public View findSnapView(RecyclerView.LayoutManager layoutManager) {
+ int childCount = layoutManager.getChildCount();
+ if (childCount == 0) {
+ return null;
+ }
+
+ // If there's only one child, then that will be the snap target.
+ if (childCount == 1) {
+ return layoutManager.getChildAt(0);
+ }
+
+ OrientationHelper orientationHelper = layoutManager.canScrollVertically()
+ ? getVerticalHelper(layoutManager)
+ : getHorizontalHelper(layoutManager);
+
+ View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
+
+ // Check if the last child visible is the last item in the list.
+ boolean lastItemVisible =
+ layoutManager.getPosition(lastVisibleChild) == layoutManager.getItemCount() - 1;
+
+ // If it is, then check how much of that view is visible.
+ float lastItemPercentageVisible = lastItemVisible
+ ? getPercentageVisible(lastVisibleChild, orientationHelper) : 0;
+
+ View closestChild = null;
+ int closestDistanceToStart = Integer.MAX_VALUE;
+ float closestPercentageVisible = 0.f;
+
+ for (int i = 0; i < childCount; i++) {
+ View child = layoutManager.getChildAt(i);
+ int startOffset = orientationHelper.getDecoratedStart(child);
+
+ if (Math.abs(startOffset) < closestDistanceToStart) {
+ float percentageVisible = getPercentageVisible(child, orientationHelper);
+
+ // Only snap to the child that is closest to the top and is more than
+ // half-way visible.
+ if (percentageVisible > VIEW_VISIBLE_THRESHOLD
+ && percentageVisible > closestPercentageVisible) {
+ closestDistanceToStart = startOffset;
+ closestChild = child;
+ closestPercentageVisible = percentageVisible;
+ }
+ }
+ }
+
+ // Snap to the last child in the list if it's the last item in the list, and it's more
+ // visible than the closest item to the top of the list.
+ return (lastItemVisible && lastItemPercentageVisible > closestPercentageVisible)
+ ? lastVisibleChild
+ : closestChild;
+ }
+
+ /**
+ * Returns the percentage of the given view that is visible, relative to its containing
+ * RecyclerView.
+ *
+ * @param view The View to get the percentage visible of.
+ * @param helper An {@link OrientationHelper} to aid with calculation.
+ * @return A float indicating the percentage of the given view that is visible.
+ */
+ private float getPercentageVisible(View view, OrientationHelper helper) {
+ int start = 0;
+ int end = helper.getEnd();
+
+ int viewStart = helper.getDecoratedStart(view);
+ int viewEnd = helper.getDecoratedEnd(view);
+
+ if (viewStart >= start && viewEnd <= end) {
+ // The view is within the bounds of the RecyclerView, so it's fully visible.
+ return 1.f;
+ } else if (viewStart <= start && viewEnd >= end) {
+ // The view is larger than the height of the RecyclerView.
+ int viewHeight = helper.getDecoratedMeasurement(view);
+ return 1.f - ((float) (Math.abs(viewStart) + Math.abs(viewEnd)) / viewHeight);
+ } else if (viewStart < start) {
+ // The view is above the start of the RecyclerView, so subtract the start offset
+ // from the total height.
+ return 1.f - ((float) Math.abs(viewStart) / helper.getDecoratedMeasurement(view));
+ } else {
+ // The view is below the end of the RecyclerView, so subtract the end offset from the
+ // total height.
+ return 1.f - ((float) Math.abs(viewEnd) / helper.getDecoratedMeasurement(view));
+ }
+ }
+
+ @Override
+ public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
+ super.attachToRecyclerView(recyclerView);
+ mRecyclerView = recyclerView;
+ }
+
+ /**
+ * Calculate the estimated scroll distance in each direction given velocities on both axes.
+ * This method will clamp the maximum scroll distance so that a single fling will never scroll
+ * more than one page.
+ *
+ * @param velocityX Fling velocity on the horizontal axis.
+ * @param velocityY Fling velocity on the vertical axis.
+ * @return An array holding the calculated distances in x and y directions respectively.
+ */
+ @Override
+ public int[] calculateScrollDistance(int velocityX, int velocityY) {
+ int[] outDist = super.calculateScrollDistance(velocityX, velocityY);
+
+ if (mRecyclerView == null) {
+ return outDist;
+ }
+
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+ if (layoutManager == null || layoutManager.getChildCount() == 0) {
+ return outDist;
+ }
+
+ int lastChildPosition = isAtEnd(layoutManager) ? 0 : layoutManager.getChildCount() - 1;
+
+ // The max and min distance is the total height of the RecyclerView minus the height of
+ // the last child. This ensures that each scroll will never scroll more than a single
+ // page on the RecyclerView. That is, the max scroll will make the last child the
+ // first child and vice versa when scrolling the opposite way.
+ int maxDistance = layoutManager.getHeight() - layoutManager.getDecoratedMeasuredHeight(
+ layoutManager.getChildAt(lastChildPosition));
+ int minDistance = -maxDistance;
+
+ outDist[0] = clamp(outDist[0], minDistance, maxDistance);
+ outDist[1] = clamp(outDist[1], minDistance, maxDistance);
+
+ return outDist;
+ }
+
+ /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
+ public boolean isAtStart(RecyclerView.LayoutManager layoutManager) {
+ if (layoutManager == null || layoutManager.getChildCount() == 0) {
+ return true;
+ }
+
+ View firstChild = layoutManager.getChildAt(0);
+ OrientationHelper orientationHelper = layoutManager.canScrollVertically()
+ ? getVerticalHelper(layoutManager)
+ : getHorizontalHelper(layoutManager);
+
+ // Check that the first child is completely visible and is the first item in the list.
+ return orientationHelper.getDecoratedStart(firstChild) >= 0
+ && layoutManager.getPosition(firstChild) == 0;
+ }
+
+ /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
+ public boolean isAtEnd(RecyclerView.LayoutManager layoutManager) {
+ if (layoutManager == null || layoutManager.getChildCount() == 0) {
+ return true;
+ }
+
+ int childCount = layoutManager.getChildCount();
+ View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
+
+ // The list has reached the bottom if the last child that is visible is the last item
+ // in the list and it's fully shown.
+ return layoutManager.getPosition(lastVisibleChild) == (layoutManager.getItemCount() - 1)
+ && layoutManager.getDecoratedBottom(lastVisibleChild) <= layoutManager.getHeight();
+ }
+
+ @NonNull
+ private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
+ if (mVerticalHelper == null || mVerticalHelper.getLayoutManager() != layoutManager) {
+ mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
+ }
+ return mVerticalHelper;
+ }
+
+ @NonNull
+ private OrientationHelper getHorizontalHelper(
+ @NonNull RecyclerView.LayoutManager layoutManager) {
+ if (mHorizontalHelper == null || mHorizontalHelper.getLayoutManager() != layoutManager) {
+ mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
+ }
+ return mHorizontalHelper;
+ }
+
+ /**
+ * Ensures that the given value falls between the range given by the min and max values. This
+ * method does not check that the min value is greater than or equal to the max value. If the
+ * parameters are not well-formed, this method's behavior is undefined.
+ *
+ * @param value The value to clamp.
+ * @param min The minimum value the given value can be.
+ * @param max The maximum value the given value can be.
+ * @return A number that falls between {@code min} or {@code max} or one of those values if the
+ * given value is less than or greater than {@code min} and {@code max} respectively.
+ */
+ private static int clamp(int value, int min, int max) {
+ return Math.max(min, Math.min(max, value));
+ }
+}
diff --git a/androidx/recyclerview/selection/MouseInputHandler.java b/androidx/recyclerview/selection/MouseInputHandler.java
index 0b4ea2ca..b6fe36b0 100644
--- a/androidx/recyclerview/selection/MouseInputHandler.java
+++ b/androidx/recyclerview/selection/MouseInputHandler.java
@@ -30,9 +30,9 @@ import android.view.MotionEvent;
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
/**
- * A MotionInputHandler that provides the high-level glue for mouse/stylus driven selection. This
+ * A MotionInputHandler that provides the high-level glue for mouse driven selection. This
* class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper}
- * to provide robust user drive selection support.
+ * to provide robust user driven selection support.
*/
final class MouseInputHandler<K> extends MotionInputHandler<K> {
diff --git a/androidx/recyclerview/selection/SelectionHelperBuilder.java b/androidx/recyclerview/selection/SelectionHelperBuilder.java
index abdefaf5..127a5116 100644
--- a/androidx/recyclerview/selection/SelectionHelperBuilder.java
+++ b/androidx/recyclerview/selection/SelectionHelperBuilder.java
@@ -88,8 +88,7 @@ public final class SelectionHelperBuilder<K> {
};
private int[] mBandToolTypes = new int[] {
- MotionEvent.TOOL_TYPE_MOUSE,
- MotionEvent.TOOL_TYPE_STYLUS
+ MotionEvent.TOOL_TYPE_MOUSE
};
public SelectionHelperBuilder(
@@ -296,7 +295,7 @@ public final class SelectionHelperBuilder<K> {
eventRouter.register(toolType, gestureHelper);
}
- // Provides high level glue for binding mouse/stylus events and gestures
+ // Provides high level glue for binding mouse events and gestures
// to selection framework.
MouseInputHandler<K> mouseHandler = new MouseInputHandler<>(
selectionHelper,
diff --git a/androidx/recyclerview/selection/SelectionStorage.java b/androidx/recyclerview/selection/SelectionStorage.java
index 81db30fd..454a76b0 100644
--- a/androidx/recyclerview/selection/SelectionStorage.java
+++ b/androidx/recyclerview/selection/SelectionStorage.java
@@ -139,7 +139,7 @@ public final class SelectionStorage<K> {
}
}
- private @Nullable Selection<String> readStringSelection(@Nullable Bundle state) {
+ private @Nullable Selection<String> readStringSelection(Bundle state) {
@Nullable ArrayList<String> stored =
state.getStringArrayList(EXTRA_SAVED_SELECTION_ENTRIES);
if (stored == null) {
@@ -151,7 +151,7 @@ public final class SelectionStorage<K> {
return selection;
}
- private @Nullable Selection<Long> readLongSelection(@Nullable Bundle state) {
+ private @Nullable Selection<Long> readLongSelection(Bundle state) {
@Nullable long[] stored = state.getLongArray(EXTRA_SAVED_SELECTION_ENTRIES);
if (stored == null) {
return null;
diff --git a/androidx/webkit/internal/codegen/BoundaryGeneration.java b/androidx/webkit/internal/codegen/BoundaryGeneration.java
new file mode 100644
index 00000000..5d92a94e
--- /dev/null
+++ b/androidx/webkit/internal/codegen/BoundaryGeneration.java
@@ -0,0 +1,39 @@
+/*
+ * 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 androidx.webkit.internal.codegen;
+
+import com.squareup.javapoet.JavaFile;
+
+import androidx.webkit.internal.codegen.representations.ClassRepr;
+
+/**
+ * Contains a bunch of utility methods for generating WebView Support Library boundary interfaces.
+ */
+public class BoundaryGeneration {
+ private static final String BOUNDARY_INTERFACE_PACKAGE = "org.chromium.support_lib_boundary";
+ private static final String INDENT = " "; // 4 white spaces.
+
+ /**
+ * Create a WebView Support Library boundary interface given the class representation
+ * {@classRepr}.
+ */
+ public static JavaFile createBoundaryInterface(ClassRepr classRepr) {
+ return JavaFile.builder(BOUNDARY_INTERFACE_PACKAGE, classRepr.createBoundaryInterface())
+ .indent(INDENT)
+ .build();
+ }
+}
diff --git a/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java b/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java
new file mode 100644
index 00000000..00736a7f
--- /dev/null
+++ b/androidx/webkit/internal/codegen/BoundaryInterfaceTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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 androidx.webkit.internal.codegen;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.lint.LintCoreProjectEnvironment;
+
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiJavaFile;
+import com.squareup.javapoet.JavaFile;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+import androidx.webkit.internal.codegen.representations.ClassRepr;
+import androidx.webkit.internal.codegen.representations.MethodRepr;
+
+@RunWith(JUnit4.class)
+public class BoundaryInterfaceTest {
+ private LintCoreProjectEnvironment mProjectEnv;
+
+ @Before
+ public void setUp() throws Exception {
+ mProjectEnv = PsiProjectSetup.sProjectEnvironment;
+ // Add files required to resolve dependencies in the tests in this class. This is needed for
+ // example to identify a class as being an android.webkit class (and turn it into an
+ // InvocationHandler).
+ mProjectEnv.registerPaths(Arrays.asList(TestUtils.getTestDepsDir()));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Test
+ public void testSingleClassAndMethod() {
+ testBoundaryInterfaceGeneration("SingleClassAndMethod");
+ }
+
+ @Test public void testWebkitReturnTypeGeneratesInvocationHandler() {
+ testBoundaryInterfaceGeneration("WebKitTypeAsMethodParameter");
+ }
+
+ @Test public void testWebkitMethodParameterTypeGeneratesInvocationHandler() {
+ testBoundaryInterfaceGeneration("WebKitTypeAsMethodReturn");
+ }
+
+ /**
+ * Ensures methods are filtered correctly so only explicitly added methods are added to the
+ * boundary interface.
+ */
+ @Test public void testFilterMethods() {
+ PsiFile inputFile =
+ TestUtils.getTestFile(mProjectEnv.getProject(), "FilterMethods");
+ PsiClass psiClass = TestUtils.getSingleClassFromFile(inputFile);
+ MethodRepr method2 =
+ MethodRepr.fromPsiMethod(psiClass.findMethodsByName("method2", false)[0]);
+ ClassRepr classRepr = new ClassRepr(Arrays.asList(method2), psiClass);
+ JavaFile actualBoundaryInterface = BoundaryGeneration.createBoundaryInterface(classRepr);
+ assertBoundaryInterfaceCorrect(psiClass.getName(), actualBoundaryInterface);
+ }
+
+ // TODO(gsennton) add test case including a (static) inner class which should create a
+ // separate boundary interface file.
+
+ /**
+ * Generates a boundary interface from the test-file with name {@param className}.java, and
+ * compares the result to the test-file {@param className}BoundaryInterface.java.
+ */
+ private void testBoundaryInterfaceGeneration(String className) {
+ PsiFile inputFile = TestUtils.getTestFile(mProjectEnv.getProject(), className);
+ ClassRepr classRepr = ClassRepr.fromPsiClass(TestUtils.getSingleClassFromFile(inputFile));
+
+ JavaFile actualBoundaryInterface = BoundaryGeneration.createBoundaryInterface(classRepr);
+ assertBoundaryInterfaceCorrect(className, actualBoundaryInterface);
+ }
+
+ private void assertBoundaryInterfaceCorrect(String className,
+ JavaFile actualBoundaryInterface) {
+ PsiJavaFile expectedBoundaryFile = TestUtils.getExpectedTestFile(mProjectEnv.getProject(),
+ className + "BoundaryInterface");
+ assertEquals(expectedBoundaryFile.getText(), actualBoundaryInterface.toString());
+ }
+}
diff --git a/androidx/webkit/internal/codegen/Main.java b/androidx/webkit/internal/codegen/Main.java
new file mode 100644
index 00000000..5a5f0f2f
--- /dev/null
+++ b/androidx/webkit/internal/codegen/Main.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 androidx.webkit.internal.codegen;
+
+/**
+ * Main entry point class for the WebView Support Library code generation tool.
+ */
+public class Main {
+
+ /**
+ * Main entry point for the WebView Support Library code generation tool.
+ */
+ public static void main(String[] args) {
+ }
+}
diff --git a/androidx/webkit/internal/codegen/PsiProjectSetup.java b/androidx/webkit/internal/codegen/PsiProjectSetup.java
new file mode 100644
index 00000000..905b1e92
--- /dev/null
+++ b/androidx/webkit/internal/codegen/PsiProjectSetup.java
@@ -0,0 +1,44 @@
+/*
+ * 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 androidx.webkit.internal.codegen;
+
+import com.android.tools.lint.LintCoreApplicationEnvironment;
+import com.android.tools.lint.LintCoreProjectEnvironment;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.util.Disposer;
+
+/**
+ * Utility class for setting up the PSI environment.
+ * PSI is normally used within IntelliJ which means parts of the interface between PSI and the rest
+ * of IntelliJ need to be initialized in a specific way, or mocked, when using PSI from the command
+ * line.
+ */
+public class PsiProjectSetup {
+ public static final LintCoreProjectEnvironment sProjectEnvironment = createProjectEnvironment();
+
+ private static LintCoreProjectEnvironment createProjectEnvironment() {
+ // Lint already uses PSI from the command line - so we copy their project configuration.
+ LintCoreApplicationEnvironment appEnv = LintCoreApplicationEnvironment.get();
+
+ Disposable parentDisposable = Disposer.newDisposable();
+ LintCoreProjectEnvironment projectEnvironment =
+ LintCoreProjectEnvironment.create(parentDisposable, appEnv);
+
+ return projectEnvironment;
+ }
+}
diff --git a/androidx/webkit/internal/codegen/TestUtils.java b/androidx/webkit/internal/codegen/TestUtils.java
new file mode 100644
index 00000000..975981ac
--- /dev/null
+++ b/androidx/webkit/internal/codegen/TestUtils.java
@@ -0,0 +1,101 @@
+/*
+ * 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 androidx.webkit.internal.codegen;
+
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.fileTypes.FileTypeRegistry;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileFactory;
+import com.intellij.psi.PsiJavaFile;
+import com.intellij.psi.PsiRecursiveElementVisitor;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+
+public class TestUtils {
+ static final String TEST_DATA_DIR = "codegen/";
+ private static final String EXPECTED_TEST_DATA_DIR = "codegen-expected/";
+
+ /**
+ * Returns the directory containing test resource file dependencies.
+ */
+ static File getTestDepsDir() {
+ return new File(TestUtils.getTestFilePath(TestUtils.TEST_DATA_DIR, "deps/"));
+ }
+
+ static PsiJavaFile getTestFile(Project project, String className) {
+ String fileName = className + ".java";
+ return createPsiFileFromFile(project, getTestFilePath(TEST_DATA_DIR, fileName));
+ }
+
+ static PsiJavaFile getExpectedTestFile(Project project, String className) {
+ String fileName = className + ".java";
+ return createPsiFileFromFile(project, getTestFilePath(EXPECTED_TEST_DATA_DIR, fileName));
+ }
+
+ private static String readEntireFile(File file) {
+ try {
+ return new String(Files.readAllBytes(file.toPath()));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static String getTestFilePath(String testDataDir, String fileName) {
+ URL resourceUrl = TestUtils.class.getClassLoader().getResource(testDataDir + fileName);
+ try {
+ return new File(resourceUrl.toURI()).getAbsolutePath();
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Use the project {@param project} to parse the file with name {@param fileName} into a PsiFile
+ * object.
+ */
+ private static PsiJavaFile createPsiFileFromFile(Project project, String fileName) {
+ FileType type = FileTypeRegistry.getInstance().getFileTypeByFileName(fileName);
+ String code = readEntireFile(new File(fileName));
+ return (PsiJavaFile)
+ PsiFileFactory.getInstance(project).createFileFromText(fileName, type, code);
+ }
+
+ /**
+ * Get a single class from a file - this should only be used as a utility method for testing.
+ */
+ static PsiClass getSingleClassFromFile(PsiFile classFile) {
+ final PsiClass[] psiClass = {null};
+ classFile.accept(new PsiRecursiveElementVisitor() {
+ @Override
+ public void visitElement(PsiElement element) {
+ if (element instanceof PsiClass) {
+ psiClass[0] = (PsiClass) element;
+ return;
+ }
+ super.visitElement(element);
+ }
+ });
+ return psiClass[0];
+ }
+}
diff --git a/androidx/webkit/internal/codegen/TypeConversionUtils.java b/androidx/webkit/internal/codegen/TypeConversionUtils.java
new file mode 100644
index 00000000..c76d6f75
--- /dev/null
+++ b/androidx/webkit/internal/codegen/TypeConversionUtils.java
@@ -0,0 +1,64 @@
+/*
+ * 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 androidx.webkit.internal.codegen;
+
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiElementFactory;
+import com.intellij.psi.PsiType;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.TypeName;
+
+/**
+ * A mix of utilities related to type conversion/recognition.
+ */
+public class TypeConversionUtils {
+ private static final String INVOCATION_HANDLER_CLASS = "java.lang.reflect.InvocationHandler";
+ private static final PsiElementFactory sPsiFactory = JavaPsiFacade.getElementFactory(
+ PsiProjectSetup.sProjectEnvironment.getProject());
+
+ /**
+ * Return a type suitable for use across the boundary - i.e. an InvocationHandler if
+ * the type @param psiType is an android.webkit type.
+ */
+ public static TypeName getBoundaryType(PsiType psiType) {
+ String typeName = psiType.getCanonicalText();
+ if (typeName.startsWith("android.webkit.")) {
+ return typeSpecFromString(
+ sPsiFactory.createTypeByFQClassName(
+ INVOCATION_HANDLER_CLASS).getCanonicalText());
+ }
+ return typeSpecFromString(typeName);
+ }
+
+ /**
+ * Return the corresponding TypeName for the String {@param type}.
+ */
+ private static TypeName typeSpecFromString(String type) {
+ switch (type) {
+ case "void": return TypeName.VOID;
+ case "boolean": return TypeName.BOOLEAN;
+ case "byte": return TypeName.BYTE;
+ case "short": return TypeName.SHORT;
+ case "int": return TypeName.INT;
+ case "long": return TypeName.LONG;
+ case "char": return TypeName.CHAR;
+ case "float": return TypeName.FLOAT;
+ case "double": return TypeName.DOUBLE;
+ default: return ClassName.bestGuess(type);
+ }
+ }
+}
diff --git a/androidx/webkit/internal/codegen/representations/ClassRepr.java b/androidx/webkit/internal/codegen/representations/ClassRepr.java
new file mode 100644
index 00000000..123770e3
--- /dev/null
+++ b/androidx/webkit/internal/codegen/representations/ClassRepr.java
@@ -0,0 +1,73 @@
+/*
+ * 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 androidx.webkit.internal.codegen.representations;
+
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiMethod;
+import com.squareup.javapoet.TypeSpec;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.lang.model.element.Modifier;
+
+/**
+ * Representation of a class for the support library to implement. Note that the class should not
+ * necessarily contain all of the members of the class we are providing a compatibility version of.
+ */
+public class ClassRepr {
+ /**
+ * List of methods to implement in the support library version of this class.
+ */
+ public final List<MethodRepr> methods;
+
+ /**
+ * Reference to the original class we want to supplement.
+ */
+ public final PsiClass psiClass;
+
+ public ClassRepr(List<MethodRepr> methods, PsiClass psiClass) {
+ this.methods = methods;
+ this.psiClass = psiClass;
+ }
+
+ /**
+ * Helper method for creating a ClassRepr containing all of the methods in the original Android
+ * class {@param psiClass}.
+ */
+ public static ClassRepr fromPsiClass(PsiClass psiClass) {
+ List<MethodRepr> methods = new ArrayList<>();
+ for (PsiMethod psiMethod : psiClass.getMethods()) {
+ methods.add(MethodRepr.fromPsiMethod(psiMethod));
+ }
+ return new ClassRepr(methods, psiClass);
+ }
+
+
+ /**
+ * Create a Boundary Interface from this ClassRepr.
+ */
+ public TypeSpec createBoundaryInterface() {
+ TypeSpec.Builder interfaceBuilder =
+ TypeSpec.interfaceBuilder(this.psiClass.getName() + "BoundaryInterface")
+ .addModifiers(Modifier.PUBLIC);
+ for (MethodRepr methodRepr : this.methods) {
+ interfaceBuilder.addMethod(methodRepr.createBoundaryInterfaceMethodDeclaration());
+ }
+ return interfaceBuilder.build();
+ }
+}
diff --git a/androidx/webkit/internal/codegen/representations/MethodRepr.java b/androidx/webkit/internal/codegen/representations/MethodRepr.java
new file mode 100644
index 00000000..1412fa55
--- /dev/null
+++ b/androidx/webkit/internal/codegen/representations/MethodRepr.java
@@ -0,0 +1,62 @@
+/*
+ * 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 androidx.webkit.internal.codegen.representations;
+
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiParameter;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterSpec;
+
+import javax.lang.model.element.Modifier;
+
+import androidx.webkit.internal.codegen.TypeConversionUtils;
+
+/**
+ * Representation of a method for the support library to implement.
+ */
+public class MethodRepr {
+ public final PsiMethod psiMethod;
+
+ private MethodRepr(PsiMethod psiMethod) {
+ this.psiMethod = psiMethod;
+ }
+
+ /**
+ * Generate a MethodRepr from a PsiMethod.
+ */
+ public static MethodRepr fromPsiMethod(PsiMethod psiMethod) {
+ return new MethodRepr(psiMethod);
+ }
+
+ /**
+ * Generate a method declaration, to be put in a boundary interface, from this MethodRepr
+ * representing an android.webkit method.
+ */
+ public MethodSpec createBoundaryInterfaceMethodDeclaration() {
+ MethodSpec.Builder builder = MethodSpec.methodBuilder(this.psiMethod.getName())
+ .returns(TypeConversionUtils.getBoundaryType(this.psiMethod.getReturnType()))
+ // The ABSTRACT modifier here ensures the method doesn't have a body.
+ .addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC);
+ for (PsiParameter param : this.psiMethod.getParameterList().getParameters()) {
+ builder.addParameter(
+ ParameterSpec.builder(
+ TypeConversionUtils.getBoundaryType(param.getType()),
+ param.getName()
+ ).build());
+ }
+ return builder.build();
+ }
+}
diff --git a/androidx/widget/ViewPager2.java b/androidx/widget/ViewPager2.java
new file mode 100644
index 00000000..9ebdea11
--- /dev/null
+++ b/androidx/widget/ViewPager2.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 androidx.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.PagerSnapHelper;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Work in progress: go/viewpager2
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class ViewPager2 extends ViewGroup {
+ // reused in layout(...)
+ private final Rect mTmpContainerRect = new Rect();
+ private final Rect mTmpChildRect = new Rect();
+
+ private RecyclerView mRecyclerView;
+
+ public ViewPager2(Context context) {
+ super(context);
+ initialize(context);
+ }
+
+ public ViewPager2(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ViewPager2(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context);
+ }
+
+ @RequiresApi(21)
+ public ViewPager2(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ // TODO(b/70663531): handle attrs, defStyleAttr, defStyleRes
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize(context);
+ }
+
+ private void initialize(Context context) {
+ mRecyclerView = new RecyclerView(context);
+
+ LinearLayoutManager layoutManager = new LinearLayoutManager(context);
+ // TODO(b/69103581): add support for vertical layout
+ // TODO(b/69398856): add support for RTL
+ layoutManager.setOrientation(RecyclerView.HORIZONTAL);
+ mRecyclerView.setLayoutManager(layoutManager);
+
+ mRecyclerView.setLayoutParams(
+ new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+ // TODO(b/70666992): add automated test for orientation change
+ new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
+
+ attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
+ }
+
+ /**
+ * TODO(b/70663708): decide on an Adapter class (for now reusing RecyclerView.Adapter)
+ *
+ * @see RecyclerView#setAdapter(Adapter)
+ */
+ public <VH extends ViewHolder> void setAdapter(final Adapter<VH> adapter) {
+ mRecyclerView.setAdapter(new Adapter<VH>() {
+ private final Adapter<VH> mAdapter = adapter;
+
+ @NonNull
+ @Override
+ public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ VH viewHolder = mAdapter.onCreateViewHolder(parent, viewType);
+
+ LayoutParams layoutParams = viewHolder.itemView.getLayoutParams();
+ if ((layoutParams.width | layoutParams.height) != LayoutParams.MATCH_PARENT) {
+ // TODO(b/70666614): decide if throw an exception or wrap in FrameLayout
+ // ourselves; consider accepting exact size equal to parent's exact size
+ throw new IllegalStateException(String.format(
+ "Item's root view must fill the whole %s (use match_parent)",
+ ViewPager2.this.getClass().getSimpleName()));
+ }
+
+ return viewHolder;
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull VH holder, int position) {
+ mAdapter.onBindViewHolder(holder, position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mAdapter.getItemCount();
+ }
+ });
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ // TODO(b/70666620): consider adding a support for Decor views
+ throw new IllegalStateException(
+ getClass().getSimpleName() + " does not support direct child views");
+ }
+
+ /** @see RecyclerView#addOnScrollListener(RecyclerView.OnScrollListener) */
+ public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
+ mRecyclerView.addOnScrollListener(listener);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // TODO(b/70666622): consider margin support
+ // TODO(b/70666626): consider delegating all this to RecyclerView
+ // TODO(b/70666625): write automated tests for this
+
+ measureChild(mRecyclerView, widthMeasureSpec, heightMeasureSpec);
+ int width = mRecyclerView.getMeasuredWidth();
+ int height = mRecyclerView.getMeasuredHeight();
+ int childState = mRecyclerView.getMeasuredState();
+
+ width += getPaddingLeft() + getPaddingRight();
+ height += getPaddingTop() + getPaddingBottom();
+
+ width = Math.max(width, getSuggestedMinimumWidth());
+ height = Math.max(height, getSuggestedMinimumHeight());
+
+ setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
+ resolveSizeAndState(height, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int width = mRecyclerView.getMeasuredWidth();
+ int height = mRecyclerView.getMeasuredHeight();
+
+ // TODO(b/70666626): consider delegating padding handling to the RecyclerView to avoid
+ // an unnatural page transition effect: http://shortn/_Vnug3yZpQT
+ mTmpContainerRect.left = getPaddingLeft();
+ mTmpContainerRect.right = r - l - getPaddingRight();
+ mTmpContainerRect.top = getPaddingTop();
+ mTmpContainerRect.bottom = b - t - getPaddingBottom();
+
+ Gravity.apply(Gravity.TOP | Gravity.START, width, height, mTmpContainerRect, mTmpChildRect);
+ mRecyclerView.layout(mTmpChildRect.left, mTmpChildRect.top, mTmpChildRect.right,
+ mTmpChildRect.bottom);
+ }
+}
diff --git a/benchmarks/GetInstancesOfClassesBenchmark.java b/benchmarks/GetInstancesOfClassesBenchmark.java
new file mode 100644
index 00000000..2e85fae0
--- /dev/null
+++ b/benchmarks/GetInstancesOfClassesBenchmark.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 benchmarks;
+
+import dalvik.system.VMDebug;
+
+public class GetInstancesOfClassesBenchmark {
+ private static class ObjectTree {
+ ObjectTree left;
+ ObjectTree right;
+
+ public ObjectTree(int depth) {
+ if (depth > 1) {
+ left = new ObjectTree(depth - 1);
+ right = new ObjectTree(depth - 1);
+ }
+ }
+ }
+
+ // 2^19 = 524288
+ private static ObjectTree tree = new ObjectTree(19);
+
+ public void timeGetInstancesOf1Class(int reps) {
+ Class[] classes = new Class[]{
+ Integer.class
+ };
+ for (int rep = 0; rep < reps; ++rep) {
+ VMDebug.getInstancesOfClasses(classes, true);
+ }
+ }
+
+ public void timeGetInstancesOf2Classes(int reps) {
+ Class[] classes = new Class[]{
+ Integer.class,
+ Long.class
+ };
+ for (int rep = 0; rep < reps; ++rep) {
+ VMDebug.getInstancesOfClasses(classes, true);
+ }
+ }
+
+ public void timeGetInstancesOf4Classes(int reps) {
+ Class[] classes = new Class[]{
+ Integer.class,
+ Long.class,
+ Float.class,
+ Double.class
+ };
+ for (int rep = 0; rep < reps; ++rep) {
+ VMDebug.getInstancesOfClasses(classes, true);
+ }
+ }
+}
diff --git a/com/android/car/setupwizardlib/CarSetupWizardLayout.java b/com/android/car/setupwizardlib/CarSetupWizardLayout.java
index 35863107..9d754f6d 100644
--- a/com/android/car/setupwizardlib/CarSetupWizardLayout.java
+++ b/com/android/car/setupwizardlib/CarSetupWizardLayout.java
@@ -18,29 +18,45 @@ package com.android.car.setupwizardlib;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
+import android.view.TouchDelegate;
import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
-
+import android.widget.TextView;
/**
- * Custom layout for the Car Setup Wizard.
+ * Custom layout for the Car Setup Wizard. Provides interfaces for setting basic functionality
+ * such as the toolbar, toolbar buttons, and themes. Any modifications to elements built by
+ * the CarSetupWizardLayout should be done through methods provided by this class unless that is
+ * not possible so as to keep the state internally consistent.
*/
public class CarSetupWizardLayout extends LinearLayout {
private View mBackButton;
- /* The Primary Continue Button should always be used when there is only a single action that
+ private TextView mToolbarTitle;
+
+ /* <p>The Primary Toolbar Button should always be used when there is only a single action that
* moves the wizard to the next screen (e.g. Only need a 'Skip' button).
*
* When there are two actions that can move the wizard to the next screen (e.g. either 'Skip'
* or 'Let's Go' are the two options), then the Primary is used for the positive action
- * while the Secondary is used for the negative action.
+ * while the Secondary is used for the negative action.</p>
+ */
+ private Button mPrimaryToolbarButton;
+ /*
+ * Flag to track the flat state.
*/
- private Button mPrimaryContinueButton;
- private Button mSecondaryContinueButton;
+ private boolean mPrimaryToolbarButtonFlat;
+ private View.OnClickListener mPrimaryToolbarButtonOnClick;
+ private Button mSecondaryToolbarButton;
+
private ProgressBar mProgressBar;
@@ -70,40 +86,48 @@ public class CarSetupWizardLayout extends LinearLayout {
0, 0);
init(attrArray);
-
}
/**
* Inflates the layout and sets the custom views (e.g. back button, continue button).
*/
- void init(TypedArray attrArray) {
+ private void init(TypedArray attrArray) {
boolean showBackButton;
- boolean showPrimaryContinueButton;
- String primaryContinueButtonText;
- boolean primaryContinueButtonEnabled;
+ boolean showToolbarTitle;
+ String toolbarTitleText;
- boolean showSecondaryContinueButton;
- String secondaryContinueButtonText;
- boolean secondaryContinueButtonEnabled;
+ boolean showPrimaryToolbarButton;
+ String primaryToolbarButtonText;
+ boolean primaryToolbarButtonEnabled;
+
+ boolean showSecondaryToolbarButton;
+ String secondaryToolbarButtonText;
+ boolean secondaryToolbarButtonEnabled;
boolean showProgressBar;
try {
showBackButton = attrArray.getBoolean(
R.styleable.CarSetupWizardLayout_showBackButton, true);
- showPrimaryContinueButton = attrArray.getBoolean(
- R.styleable.CarSetupWizardLayout_showPrimaryContinueButton, true);
- primaryContinueButtonText = attrArray.getString(
- R.styleable.CarSetupWizardLayout_primaryContinueButtonText);
- primaryContinueButtonEnabled = attrArray.getBoolean(
- R.styleable.CarSetupWizardLayout_primaryContinueButtonEnabled, true);
- showSecondaryContinueButton = attrArray.getBoolean(
- R.styleable.CarSetupWizardLayout_showSecondaryContinueButton, false);
- secondaryContinueButtonText = attrArray.getString(
- R.styleable.CarSetupWizardLayout_secondaryContinueButtonText);
- secondaryContinueButtonEnabled = attrArray.getBoolean(
- R.styleable.CarSetupWizardLayout_secondaryContinueButtonEnabled, true);
+ showToolbarTitle = attrArray.getBoolean(
+ R.styleable.CarSetupWizardLayout_showToolbarTitle, false);
+ toolbarTitleText = attrArray.getString(
+ R.styleable.CarSetupWizardLayout_toolbarTitleText);
+ showPrimaryToolbarButton = attrArray.getBoolean(
+ R.styleable.CarSetupWizardLayout_showPrimaryToolbarButton, true);
+ primaryToolbarButtonText = attrArray.getString(
+ R.styleable.CarSetupWizardLayout_primaryToolbarButtonText);
+ primaryToolbarButtonEnabled = attrArray.getBoolean(
+ R.styleable.CarSetupWizardLayout_primaryToolbarButtonEnabled, true);
+ mPrimaryToolbarButtonFlat = attrArray.getBoolean(
+ R.styleable.CarSetupWizardLayout_primaryToolbarButtonFlat, false);
+ showSecondaryToolbarButton = attrArray.getBoolean(
+ R.styleable.CarSetupWizardLayout_showSecondaryToolbarButton, false);
+ secondaryToolbarButtonText = attrArray.getString(
+ R.styleable.CarSetupWizardLayout_secondaryToolbarButtonText);
+ secondaryToolbarButtonEnabled = attrArray.getBoolean(
+ R.styleable.CarSetupWizardLayout_secondaryToolbarButtonEnabled, true);
showProgressBar = attrArray.getBoolean(
R.styleable.CarSetupWizardLayout_showProgressBar, false);
} finally {
@@ -115,31 +139,48 @@ public class CarSetupWizardLayout extends LinearLayout {
// Set the back button visibility based on the custom attribute.
mBackButton = findViewById(R.id.back_button);
- if (!showBackButton) {
- setBackButtonVisible(false);
+ setBackButtonVisible(showBackButton);
+
+ // Set the toolbar title visibility and text based on the custom attributes.
+ mToolbarTitle = findViewById(R.id.toolbar_title);
+ if (showToolbarTitle) {
+ setToolbarTitleText(toolbarTitleText);
+ } else {
+ setToolbarTitleVisible(false);
}
// Set the primary continue button visibility and text based on the custom attributes.
- mPrimaryContinueButton = findViewById(R.id.primary_continue_button);
- if (showPrimaryContinueButton) {
- setPrimaryContinueButtonText(primaryContinueButtonText);
- setPrimaryContinueButtonEnabled(primaryContinueButtonEnabled);
+ ViewStub primaryToolbarButtonStub =
+ (ViewStub) findViewById(R.id.primary_toolbar_button_stub);
+ // Set the button layout to flat if that attribute was set.
+ if (mPrimaryToolbarButtonFlat) {
+ primaryToolbarButtonStub.setLayoutResource(R.layout.flat_button);
+ }
+ primaryToolbarButtonStub.inflate();
+ mPrimaryToolbarButton = findViewById(R.id.primary_toolbar_button);
+ if (showPrimaryToolbarButton) {
+ setPrimaryToolbarButtonText(primaryToolbarButtonText);
+ setPrimaryToolbarButtonEnabled(primaryToolbarButtonEnabled);
} else {
- setPrimaryContinueButtonVisible(false);
+ setPrimaryToolbarButtonVisible(false);
}
// Set the secondary continue button visibility and text based on the custom attributes.
- mSecondaryContinueButton = findViewById(R.id.secondary_continue_button);
- if (showSecondaryContinueButton) {
- setSecondaryContinueButtonText(secondaryContinueButtonText);
- setSecondaryContinueButtonEnabled(secondaryContinueButtonEnabled);
- } else {
- setSecondaryContinueButtonVisible(false);
+ ViewStub secondaryToolbarButtonStub =
+ (ViewStub) findViewById(R.id.secondary_toolbar_button_stub);
+ if (showSecondaryToolbarButton || !TextUtils.isEmpty(secondaryToolbarButtonText)) {
+ secondaryToolbarButtonStub.inflate();
+ mSecondaryToolbarButton = findViewById(R.id.secondary_toolbar_button);
+ setSecondaryToolbarButtonText(secondaryToolbarButtonText);
+ setSecondaryToolbarButtonEnabled(secondaryToolbarButtonEnabled);
+ setSecondaryToolbarButtonVisible(showSecondaryToolbarButton);
}
mProgressBar = findViewById(R.id.progress_bar);
setProgressBarVisible(showProgressBar);
+ // Set orientation programmatically since the inflated layout uses <merge>
+ setOrientation(LinearLayout.VERTICAL);
}
/**
@@ -157,69 +198,172 @@ public class CarSetupWizardLayout extends LinearLayout {
mBackButton.setOnClickListener(listener);
}
+ // Add or remove the back button touch delegate depending on whether it is visible.
+ private void updateBackButtonTouchDelegate(boolean visible) {
+ if (visible) {
+ // Post this action in the parent's message queue to make sure the parent
+ // lays out its children before getHitRect() is called
+ this.post(new Runnable() {
+ @Override
+ public void run() {
+ Rect delegateArea = new Rect();
+
+ mBackButton.getHitRect(delegateArea);
+
+ /*
+ * Update the delegate area based on the difference between the current size and
+ * the touch target size
+ */
+ float touchTargetSize = getResources().getDimension(
+ R.dimen.car_touch_target_size);
+ float primaryIconSize = getResources().getDimension(
+ R.dimen.car_primary_icon_size);
+
+ int sizeDifference = (int) ((touchTargetSize - primaryIconSize) / 2);
+
+ delegateArea.right += sizeDifference;
+ delegateArea.bottom += sizeDifference;
+ delegateArea.left -= sizeDifference;
+ delegateArea.top -= sizeDifference;
+
+ // Set the TouchDelegate on the parent view
+ TouchDelegate touchDelegate = new TouchDelegate(delegateArea,
+ mBackButton);
+
+ if (View.class.isInstance(mBackButton.getParent())) {
+ ((View) mBackButton.getParent()).setTouchDelegate(touchDelegate);
+ }
+ }
+ });
+ } else {
+ // Set the TouchDelegate to null if the back button is not visible.
+ if (View.class.isInstance(mBackButton.getParent())) {
+ ((View) mBackButton.getParent()).setTouchDelegate(null);
+ }
+ }
+ }
+
/**
* Set the back button visibility to the given visibility.
*/
public void setBackButtonVisible(boolean visible) {
setViewVisible(mBackButton, visible);
+ updateBackButtonTouchDelegate(visible);
+ }
+
+ /**
+ * Sets the header title visibility to given value.
+ */
+ public void setToolbarTitleVisible(boolean visible) {
+ setViewVisible(mToolbarTitle, visible);
+ }
+
+ /**
+ * Sets the header title text to the provided text.
+ */
+ public void setToolbarTitleText(String text) {
+ mToolbarTitle.setText(text);
}
/**
* Set whether the primary continue button is enabled.
*/
- public void setPrimaryContinueButtonEnabled(boolean enabled) {
- mPrimaryContinueButton.setEnabled(enabled);
+ public void setPrimaryToolbarButtonEnabled(boolean enabled) {
+ mPrimaryToolbarButton.setEnabled(enabled);
}
/**
* Set the primary continue button onClickListener to the given listener. Can be null if the
- * listener should be overridden so no callback is made.
+ * listener should be overridden so no callback is made. All changes to primary toolbar
+ * button's onClickListener should be made here so they can be stored through changes to the
+ * button.
*/
- public void setPrimaryContinueButtonListener(@Nullable View.OnClickListener listener) {
- mPrimaryContinueButton.setOnClickListener(listener);
+ public void setPrimaryToolbarButtonListener(@Nullable View.OnClickListener listener) {
+ mPrimaryToolbarButtonOnClick = listener;
+ mPrimaryToolbarButton.setOnClickListener(listener);
}
/**
* Set the primary continue button text to the given text.
*/
- public void setPrimaryContinueButtonText(String text) {
- mPrimaryContinueButton.setText(text);
+ public void setPrimaryToolbarButtonText(String text) {
+ mPrimaryToolbarButton.setText(text);
}
/**
* Set the primary continue button visibility to the given visibility.
*/
- public void setPrimaryContinueButtonVisible(boolean visible) {
- setViewVisible(mPrimaryContinueButton, visible);
+ public void setPrimaryToolbarButtonVisible(boolean visible) {
+ setViewVisible(mPrimaryToolbarButton, visible);
+ }
+
+ /**
+ * Changes the button in the primary slot to a flat theme, maintaining the text, visibility,
+ * whether it is enabled, and id.
+ * <p>NOTE: that other attributes set manually on the primaryToolbarButton will be lost on calls
+ * to this method as the button will be replaced.</p>
+ */
+ public void setPrimaryToolbarButtonFlat(boolean isFlat) {
+ // Do nothing if the state isn't changing.
+ if (isFlat == mPrimaryToolbarButtonFlat) {
+ return;
+ }
+ int layoutId = isFlat ? R.layout.flat_button : R.layout.primary_button;
+ Button newPrimaryButton = (Button) inflate(mContext, layoutId, null);
+ newPrimaryButton.setId(mPrimaryToolbarButton.getId());
+ newPrimaryButton.setVisibility(mPrimaryToolbarButton.getVisibility());
+ newPrimaryButton.setEnabled(mPrimaryToolbarButton.isEnabled());
+ newPrimaryButton.setText(mPrimaryToolbarButton.getText());
+ if (mPrimaryToolbarButtonOnClick != null) {
+ newPrimaryButton.setOnClickListener(mPrimaryToolbarButtonOnClick);
+ }
+ newPrimaryButton.setLayoutParams(mPrimaryToolbarButton.getLayoutParams());
+
+ ViewGroup parent = (ViewGroup) findViewById(R.id.button_container);
+ int buttonIndex = parent.indexOfChild(mPrimaryToolbarButton);
+ parent.removeViewAt(buttonIndex);
+ parent.addView(newPrimaryButton, buttonIndex);
+
+ // Update state of layout
+ mPrimaryToolbarButton = newPrimaryButton;
+ mPrimaryToolbarButtonFlat = isFlat;
}
/**
* Set whether the secondary continue button is enabled.
*/
- public void setSecondaryContinueButtonEnabled(boolean enabled){
- mSecondaryContinueButton.setEnabled(enabled);
+ public void setSecondaryToolbarButtonEnabled(boolean enabled) {
+ maybeInflateSecondaryToolbarButton();
+ mSecondaryToolbarButton.setEnabled(enabled);
}
/**
* Set the secondary continue button onClickListener to the given listener. Can be null if the
* listener should be overridden so no callback is made.
*/
- public void setSecondaryContinueButtonListener(@Nullable View.OnClickListener listener) {
- mSecondaryContinueButton.setOnClickListener(listener);
+ public void setSecondaryToolbarButtonListener(@Nullable View.OnClickListener listener) {
+ maybeInflateSecondaryToolbarButton();
+ mSecondaryToolbarButton.setOnClickListener(listener);
}
/**
* Set the secondary continue button text to the given text.
*/
- public void setSecondaryContinueButtonText(String text) {
- mSecondaryContinueButton.setText(text);
+ public void setSecondaryToolbarButtonText(String text) {
+ maybeInflateSecondaryToolbarButton();
+ mSecondaryToolbarButton.setText(text);
}
/**
* Set the secondary continue button visibility to the given visibility.
*/
- public void setSecondaryContinueButtonVisible(boolean visible) {
- setViewVisible(mSecondaryContinueButton, visible);
+ public void setSecondaryToolbarButtonVisible(boolean visible) {
+ // If not setting it visible and it hasn't been inflated yet then don't inflate.
+ if (!visible && mSecondaryToolbarButton == null) {
+ return;
+ }
+ maybeInflateSecondaryToolbarButton();
+ setViewVisible(mSecondaryToolbarButton, visible);
}
/**
@@ -228,4 +372,21 @@ public class CarSetupWizardLayout extends LinearLayout {
public void setProgressBarVisible(boolean visible) {
setViewVisible(mProgressBar, visible);
}
+
+ /**
+ * A method that will inflate the SecondaryToolbarButton if it is has not already been
+ * inflated. If it has been inflated already this method will do nothing.
+ */
+ private void maybeInflateSecondaryToolbarButton() {
+ ViewStub secondaryToolbarButtonStub =
+ (ViewStub) findViewById(R.id.secondary_toolbar_button_stub);
+ // If the secondaryToolbarButtonStub is null then the stub has been inflated so there is
+ // nothing to do.
+ if (secondaryToolbarButtonStub != null) {
+ secondaryToolbarButtonStub.inflate();
+ mSecondaryToolbarButton = (Button) findViewById(R.id.secondary_toolbar_button);
+ setSecondaryToolbarButtonVisible(false);
+ }
+
+ }
}
diff --git a/com/android/car/setupwizardlib/util/CarWizardManagerHelper.java b/com/android/car/setupwizardlib/util/CarWizardManagerHelper.java
new file mode 100644
index 00000000..72d5a5ae
--- /dev/null
+++ b/com/android/car/setupwizardlib/util/CarWizardManagerHelper.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 com.android.car.setupwizardlib.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+
+/**
+ * <p>Derived from {@code com.android.setupwizardlib/WizardManagerHelper.java}
+ */
+public final class CarWizardManagerHelper {
+ static final String EXTRA_WIZARD_BUNDLE = "wizardBundle";
+ static final String EXTRA_IS_FIRST_RUN = "firstRun";
+ private static final String ACTION_NEXT = "com.android.wizard.NEXT";
+ private static final String EXTRA_RESULT_CODE = "com.android.setupwizard.ResultCode";
+
+ private CarWizardManagerHelper() {
+ }
+
+ /**
+ * Get an intent that will invoke the next step of setup wizard.
+ *
+ * @param originalIntent The original intent that was used to start the step, usually via
+ * {@link android.app.Activity#getIntent()}.
+ * @param resultCode The result code of the step. See {@link ResultCodes}.
+ * @return A new intent that can be used with
+ * {@link android.app.Activity#startActivityForResult(Intent, int)} to start the next
+ * step of the setup flow.
+ */
+ public static Intent getNextIntent(Intent originalIntent, int resultCode) {
+ return getNextIntent(originalIntent, resultCode, null);
+ }
+
+ /**
+ * Get an intent that will invoke the next step of setup wizard.
+ *
+ * @param originalIntent The original intent that was used to start the step, usually via
+ * {@link android.app.Activity#getIntent()}.
+ * @param resultCode The result code of the step. See {@link ResultCodes}.
+ * @param data An intent containing extra result data.
+ * @return A new intent that can be used with
+ * {@link android.app.Activity#startActivityForResult(Intent, int)} to start the next
+ * step of the setup flow.
+ */
+ public static Intent getNextIntent(Intent originalIntent, int resultCode, Intent data) {
+ Intent intent = new Intent(ACTION_NEXT);
+ copyWizardManagerExtras(originalIntent, intent);
+ intent.putExtra(EXTRA_RESULT_CODE, resultCode);
+ if (data != null && data.getExtras() != null) {
+ intent.putExtras(data.getExtras());
+ }
+
+ return intent;
+ }
+
+ /**
+ * Copy the internal extras used by setup wizard from one intent to another. For low-level use
+ * only, such as when using {@link Intent#FLAG_ACTIVITY_FORWARD_RESULT} to relay to another
+ * intent.
+ *
+ * @param srcIntent Intent to get the wizard manager extras from.
+ * @param dstIntent Intent to copy the wizard manager extras to.
+ */
+ public static void copyWizardManagerExtras(Intent srcIntent, Intent dstIntent) {
+ dstIntent.putExtra(EXTRA_WIZARD_BUNDLE, srcIntent.getBundleExtra(EXTRA_WIZARD_BUNDLE));
+ dstIntent.putExtra(EXTRA_IS_FIRST_RUN,
+ srcIntent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false));
+ }
+
+ /**
+ * Check whether an intent is intended to be used within the setup wizard flow.
+ *
+ * @param intent The intent to be checked, usually from
+ * {@link android.app.Activity#getIntent()}.
+ * @return true if the intent passed in was intended to be used with setup wizard.
+ */
+ public static boolean isSetupWizardIntent(Intent intent) {
+ return intent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false);
+ }
+
+ /**
+ * Checks whether the current user has completed Setup Wizard. This is true if the current user
+ * has gone through Setup Wizard. The current user may or may not be the device owner and the
+ * device owner may have already completed setup wizard.
+ *
+ * @param context The context to retrieve the settings.
+ * @return true if the current user has completed Setup Wizard.
+ * @see #isDeviceProvisioned(Context)
+ */
+ public static boolean isUserSetupComplete(Context context) {
+ return Settings.Secure.getInt(context.getContentResolver(),
+ Settings.Secure.USER_SETUP_COMPLETE, 0) == 1;
+ }
+
+ /**
+ * Checks whether the device is provisioned. This means that the device has gone through Setup
+ * Wizard at least once. Note that the user can still be in Setup Wizard even if this is true,
+ * for a secondary user profile triggered through Settings > Add account.
+ *
+ * @param context The context to retrieve the settings.
+ * @return true if the device is provisioned.
+ * @see #isUserSetupComplete(Context)
+ */
+ public static boolean isDeviceProvisioned(Context context) {
+ return Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.DEVICE_PROVISIONED, 0) == 1;
+ }
+}
diff --git a/com/android/car/setupwizardlib/util/ResultCodes.java b/com/android/car/setupwizardlib/util/ResultCodes.java
new file mode 100644
index 00000000..604e8b0a
--- /dev/null
+++ b/com/android/car/setupwizardlib/util/ResultCodes.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.android.car.setupwizardlib.util;
+
+import static android.app.Activity.RESULT_FIRST_USER;
+
+/**
+ * Wizard result codes that map to the appropriate Activity/FragmentActivity result codes
+ */
+public final class ResultCodes {
+ public static final int RESULT_SKIP = RESULT_FIRST_USER;
+ public static final int RESULT_ACTIVITY_NOT_FOUND = RESULT_FIRST_USER + 2;
+ public static final int RESULT_FIRST_SETUP_USER = RESULT_FIRST_USER + 100;
+
+ private ResultCodes() {
+ }
+}
diff --git a/com/android/commands/content/Content.java b/com/android/commands/content/Content.java
index 716bc5f2..f75678b7 100644
--- a/com/android/commands/content/Content.java
+++ b/com/android/commands/content/Content.java
@@ -32,12 +32,10 @@ import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import libcore.io.Streams;
-import libcore.io.IoUtils;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
/**
* This class is a command line utility for manipulating content. A client
@@ -122,13 +120,14 @@ public class Content {
+ "\n"
+ "usage: adb shell content read --uri <URI> [--user <USER_ID>]\n"
+ " Example:\n"
- + " # cat default ringtone to a file, then pull to host\n"
- + " adb shell 'content read --uri content://settings/system/ringtone >"
- + " /mnt/sdcard/tmp.ogg' && adb pull /mnt/sdcard/tmp.ogg\n"
+ + " adb shell 'content read --uri content://settings/system/ringtone_cache' > host.ogg\n"
+ + "\n"
+ + "usage: adb shell content write --uri <URI> [--user <USER_ID>]\n"
+ + " Example:\n"
+ + " adb shell 'content write --uri content://settings/system/ringtone_cache' < host.ogg\n"
+ "\n"
+ "usage: adb shell content gettype --uri <URI> [--user <USER_ID>]\n"
+ " Example:\n"
- + " # Show the mime-type of the URI\n"
+ " adb shell content gettype --uri content://media/internal/audio/media/\n"
+ "\n";
@@ -139,6 +138,7 @@ public class Content {
private static final String ARGUMENT_QUERY = "query";
private static final String ARGUMENT_CALL = "call";
private static final String ARGUMENT_READ = "read";
+ private static final String ARGUMENT_WRITE = "write";
private static final String ARGUMENT_GET_TYPE = "gettype";
private static final String ARGUMENT_WHERE = "--where";
private static final String ARGUMENT_BIND = "--bind";
@@ -179,6 +179,8 @@ public class Content {
return parseCallCommand();
} else if (ARGUMENT_READ.equals(operation)) {
return parseReadCommand();
+ } else if (ARGUMENT_WRITE.equals(operation)) {
+ return parseWriteCommand();
} else if (ARGUMENT_GET_TYPE.equals(operation)) {
return parseGetTypeCommand();
} else {
@@ -339,6 +341,25 @@ public class Content {
return new ReadCommand(uri, userId);
}
+ private WriteCommand parseWriteCommand() {
+ Uri uri = null;
+ int userId = UserHandle.USER_SYSTEM;
+ for (String argument; (argument = mTokenizer.nextArg())!= null;) {
+ if (ARGUMENT_URI.equals(argument)) {
+ uri = Uri.parse(argumentValueRequired(argument));
+ } else if (ARGUMENT_USER.equals(argument)) {
+ userId = Integer.parseInt(argumentValueRequired(argument));
+ } else {
+ throw new IllegalArgumentException("Unsupported argument: " + argument);
+ }
+ }
+ if (uri == null) {
+ throw new IllegalArgumentException("Content provider URI not specified."
+ + " Did you specify --uri argument?");
+ }
+ return new WriteCommand(uri, userId);
+ }
+
public QueryCommand parseQueryCommand() {
Uri uri = null;
int userId = UserHandle.USER_SYSTEM;
@@ -561,20 +582,21 @@ public class Content {
@Override
public void onExecute(IContentProvider provider) throws Exception {
- final ParcelFileDescriptor fd = provider.openFile(null, mUri, "r", null, null);
- copy(new FileInputStream(fd.getFileDescriptor()), System.out);
+ try (ParcelFileDescriptor fd = provider.openFile(null, mUri, "r", null, null)) {
+ Streams.copy(new FileInputStream(fd.getFileDescriptor()), System.out);
+ }
}
+ }
- private static void copy(InputStream is, OutputStream os) throws IOException {
- final byte[] buffer = new byte[8 * 1024];
- int read;
- try {
- while ((read = is.read(buffer)) > -1) {
- os.write(buffer, 0, read);
- }
- } finally {
- IoUtils.closeQuietly(is);
- IoUtils.closeQuietly(os);
+ private static class WriteCommand extends Command {
+ public WriteCommand(Uri uri, int userId) {
+ super(uri, userId);
+ }
+
+ @Override
+ public void onExecute(IContentProvider provider) throws Exception {
+ try (ParcelFileDescriptor fd = provider.openFile(null, mUri, "w", null, null)) {
+ Streams.copy(System.in, new FileOutputStream(fd.getFileDescriptor()));
}
}
}
diff --git a/com/android/commands/ime/Ime.java b/com/android/commands/ime/Ime.java
deleted file mode 100644
index 72a0af6c..00000000
--- a/com/android/commands/ime/Ime.java
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.commands.ime;
-
-import com.android.internal.view.IInputMethodManager;
-
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.util.PrintStreamPrinter;
-import android.util.Printer;
-import android.view.inputmethod.InputMethodInfo;
-
-import java.util.List;
-
-public final class Ime {
- IInputMethodManager mImm;
-
- private String[] mArgs;
- private int mNextArg;
- private String mCurArgData;
-
- private static final String IMM_NOT_RUNNING_ERR =
- "Error: Could not access the Input Method Manager. Is the system running?";
-
- public static void main(String[] args) {
- new Ime().run(args);
- }
-
- public void run(String[] args) {
- if (args.length < 1) {
- showUsage();
- return;
- }
-
- mImm = IInputMethodManager.Stub.asInterface(ServiceManager.getService("input_method"));
- if (mImm == null) {
- System.err.println(IMM_NOT_RUNNING_ERR);
- return;
- }
-
- mArgs = args;
- String op = args[0];
- mNextArg = 1;
-
- if ("list".equals(op)) {
- runList();
- return;
- }
-
- if ("enable".equals(op)) {
- runSetEnabled(true);
- return;
- }
-
- if ("disable".equals(op)) {
- runSetEnabled(false);
- return;
- }
-
- if ("set".equals(op)) {
- runSet();
- return;
- }
-
- if (op != null) {
- System.err.println("Error: unknown command '" + op + "'");
- }
- showUsage();
- }
-
- /**
- * Execute the list sub-command.
- */
- private void runList() {
- String opt;
- boolean all = false;
- boolean brief = false;
- while ((opt=nextOption()) != null) {
- if (opt.equals("-a")) {
- all = true;
- } else if (opt.equals("-s")) {
- brief = true;
- } else {
- System.err.println("Error: Unknown option: " + opt);
- showUsage();
- return;
- }
- }
-
-
- List<InputMethodInfo> methods;
- if (!all) {
- try {
- methods = mImm.getEnabledInputMethodList();
- } catch (RemoteException e) {
- System.err.println(e.toString());
- System.err.println(IMM_NOT_RUNNING_ERR);
- return;
- }
- } else {
- try {
- methods = mImm.getInputMethodList();
- } catch (RemoteException e) {
- System.err.println(e.toString());
- System.err.println(IMM_NOT_RUNNING_ERR);
- return;
- }
- }
-
- if (methods != null) {
- Printer pr = new PrintStreamPrinter(System.out);
- for (int i=0; i<methods.size(); i++) {
- InputMethodInfo imi = methods.get(i);
- if (brief) {
- System.out.println(imi.getId());
- } else {
- System.out.println(imi.getId() + ":");
- imi.dump(pr, " ");
- }
- }
- }
- }
-
- private void runSetEnabled(boolean state) {
- String id = nextArg();
- if (id == null) {
- System.err.println("Error: no input method ID specified");
- showUsage();
- return;
- }
-
- try {
- boolean res = mImm.setInputMethodEnabled(id, state);
- if (state) {
- System.out.println("Input method " + id + ": "
- + (res ? "already enabled" : "now enabled"));
- } else {
- System.out.println("Input method " + id + ": "
- + (res ? "now disabled" : "already disabled"));
- }
- } catch (IllegalArgumentException e) {
- System.err.println("Error: " + e.getMessage());
- return;
- } catch (RemoteException e) {
- System.err.println(e.toString());
- System.err.println(IMM_NOT_RUNNING_ERR);
- return;
- }
- }
-
- private void runSet() {
- String id = nextArg();
- if (id == null) {
- System.err.println("Error: no input method ID specified");
- showUsage();
- return;
- }
-
- try {
- mImm.setInputMethod(null, id);
- System.out.println("Input method " + id + " selected");
- } catch (IllegalArgumentException e) {
- System.err.println("Error: " + e.getMessage());
- return;
- } catch (RemoteException e) {
- System.err.println(e.toString());
- System.err.println(IMM_NOT_RUNNING_ERR);
- return;
- }
- }
-
- private String nextOption() {
- if (mNextArg >= mArgs.length) {
- return null;
- }
- String arg = mArgs[mNextArg];
- if (!arg.startsWith("-")) {
- return null;
- }
- mNextArg++;
- if (arg.equals("--")) {
- return null;
- }
- if (arg.length() > 1 && arg.charAt(1) != '-') {
- if (arg.length() > 2) {
- mCurArgData = arg.substring(2);
- return arg.substring(0, 2);
- } else {
- mCurArgData = null;
- return arg;
- }
- }
- mCurArgData = null;
- return arg;
- }
-
- private String nextOptionData() {
- if (mCurArgData != null) {
- return mCurArgData;
- }
- if (mNextArg >= mArgs.length) {
- return null;
- }
- String data = mArgs[mNextArg];
- mNextArg++;
- return data;
- }
-
- private String nextArg() {
- if (mNextArg >= mArgs.length) {
- return null;
- }
- String arg = mArgs[mNextArg];
- mNextArg++;
- return arg;
- }
-
- private static void showUsage() {
- System.err.println("usage: ime list [-a] [-s]");
- System.err.println(" ime enable ID");
- System.err.println(" ime disable ID");
- System.err.println(" ime set ID");
- System.err.println("");
- System.err.println("The list command prints all enabled input methods. Use");
- System.err.println("the -a option to see all input methods. Use");
- System.err.println("the -s option to see only a single summary line of each.");
- System.err.println("");
- System.err.println("The enable command allows the given input method ID to be used.");
- System.err.println("");
- System.err.println("The disable command disallows the given input method ID from use.");
- System.err.println("");
- System.err.println("The set command switches to the given input method ID.");
- }
-}
diff --git a/com/android/commands/input/Input.java b/com/android/commands/input/Input.java
index 9ee11f85..d3ec3207 100644
--- a/com/android/commands/input/Input.java
+++ b/com/android/commands/input/Input.java
@@ -88,8 +88,8 @@ public class Input {
final boolean longpress = "--longpress".equals(args[index + 1]);
final int start = longpress ? index + 2 : index + 1;
inputSource = getSource(inputSource, InputDevice.SOURCE_KEYBOARD);
- if (length > start) {
- for (int i = start; i < length; i++) {
+ if (args.length > start) {
+ for (int i = start; i < args.length; i++) {
int keyCode = KeyEvent.keyCodeFromString(args[i]);
if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
keyCode = KeyEvent.keyCodeFromString("KEYCODE_" + args[i]);
diff --git a/com/android/commands/wm/Wm.java b/com/android/commands/wm/Wm.java
deleted file mode 100644
index 8defb331..00000000
--- a/com/android/commands/wm/Wm.java
+++ /dev/null
@@ -1,296 +0,0 @@
-/*
-**
-** Copyright 2013, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-** http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
-
-
-package com.android.commands.wm;
-
-import android.content.Context;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.os.ParcelFileDescriptor;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.UserHandle;
-import android.util.AndroidException;
-import android.util.DisplayMetrics;
-import android.system.Os;
-import android.view.Display;
-import android.view.IWindowManager;
-import com.android.internal.os.BaseCommand;
-
-import java.io.FileDescriptor;
-import java.io.FileInputStream;
-import java.io.DataInputStream;
-import java.io.PrintStream;
-import java.lang.Runtime;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class Wm extends BaseCommand {
-
- private IWindowManager mWm;
-
- /**
- * Command-line entry point.
- *
- * @param args The command-line arguments
- */
- public static void main(String[] args) {
- (new Wm()).run(args);
- }
-
- @Override
- public void onShowUsage(PrintStream out) {
- out.println(
- "usage: wm [subcommand] [options]\n" +
- " wm size [reset|WxH|WdpxHdp]\n" +
- " wm density [reset|DENSITY]\n" +
- " wm overscan [reset|LEFT,TOP,RIGHT,BOTTOM]\n" +
- " wm scaling [off|auto]\n" +
- " wm screen-capture [userId] [true|false]\n" +
- "\n" +
- "wm size: return or override display size.\n" +
- " width and height in pixels unless suffixed with 'dp'.\n" +
- "\n" +
- "wm density: override display density.\n" +
- "\n" +
- "wm overscan: set overscan area for display.\n" +
- "\n" +
- "wm scaling: set display scaling mode.\n" +
- "\n" +
- "wm screen-capture: enable/disable screen capture.\n" +
- "\n" +
- "wm dismiss-keyguard: dismiss the keyguard, prompting the user for auth if " +
- "necessary.\n" +
- "\n" +
- "wm surface-trace: log surface commands to stdout in a binary format.\n"
- );
- }
-
- @Override
- public void onRun() throws Exception {
- mWm = IWindowManager.Stub.asInterface(ServiceManager.checkService(
- Context.WINDOW_SERVICE));
- if (mWm == null) {
- System.err.println(NO_SYSTEM_ERROR_CODE);
- throw new AndroidException("Can't connect to window manager; is the system running?");
- }
-
- String op = nextArgRequired();
-
- if (op.equals("size")) {
- runDisplaySize();
- } else if (op.equals("density")) {
- runDisplayDensity();
- } else if (op.equals("overscan")) {
- runDisplayOverscan();
- } else if (op.equals("scaling")) {
- runDisplayScaling();
- } else if (op.equals("screen-capture")) {
- runSetScreenCapture();
- } else if (op.equals("dismiss-keyguard")) {
- runDismissKeyguard();
- } else if (op.equals("surface-trace")) {
- runSurfaceTrace();
- } else {
- showError("Error: unknown command '" + op + "'");
- return;
- }
- }
-
- private void runSurfaceTrace() throws Exception {
- ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(FileDescriptor.out);
- mWm.enableSurfaceTrace(pfd);
-
- try {
- // No one is going to wake us up, we are just waiting on SIGINT. Otherwise
- // the WM can happily continue writing to our stdout.
- synchronized (this) {
- this.wait();
- }
- } finally {
- mWm.disableSurfaceTrace();
- }
- }
-
- private void runSetScreenCapture() throws Exception {
- String userIdStr = nextArg();
- String enableStr = nextArg();
- int userId;
- boolean disable;
-
- try {
- userId = Integer.parseInt(userIdStr);
- } catch (NumberFormatException e) {
- System.err.println("Error: bad number " + e);
- return;
- }
-
- disable = !Boolean.parseBoolean(enableStr);
-
- try {
- mWm.setScreenCaptureDisabled(userId, disable);
- } catch (RemoteException e) {
- System.err.println("Error: Can't set screen capture " + e);
- }
- }
-
- private void runDisplaySize() throws Exception {
- String size = nextArg();
- int w, h;
- if (size == null) {
- Point initialSize = new Point();
- Point baseSize = new Point();
- try {
- mWm.getInitialDisplaySize(Display.DEFAULT_DISPLAY, initialSize);
- mWm.getBaseDisplaySize(Display.DEFAULT_DISPLAY, baseSize);
- System.out.println("Physical size: " + initialSize.x + "x" + initialSize.y);
- if (!initialSize.equals(baseSize)) {
- System.out.println("Override size: " + baseSize.x + "x" + baseSize.y);
- }
- } catch (RemoteException e) {
- }
- return;
- } else if ("reset".equals(size)) {
- w = h = -1;
- } else {
- int div = size.indexOf('x');
- if (div <= 0 || div >= (size.length()-1)) {
- System.err.println("Error: bad size " + size);
- return;
- }
- String wstr = size.substring(0, div);
- String hstr = size.substring(div+1);
- try {
- w = parseDimension(wstr);
- h = parseDimension(hstr);
- } catch (NumberFormatException e) {
- System.err.println("Error: bad number " + e);
- return;
- }
- }
-
- try {
- if (w >= 0 && h >= 0) {
- // TODO(multidisplay): For now Configuration only applies to main screen.
- mWm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, w, h);
- } else {
- mWm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY);
- }
- } catch (RemoteException e) {
- }
- }
-
- private void runDisplayDensity() throws Exception {
- String densityStr = nextArg();
- int density;
- if (densityStr == null) {
- try {
- int initialDensity = mWm.getInitialDisplayDensity(Display.DEFAULT_DISPLAY);
- int baseDensity = mWm.getBaseDisplayDensity(Display.DEFAULT_DISPLAY);
- System.out.println("Physical density: " + initialDensity);
- if (initialDensity != baseDensity) {
- System.out.println("Override density: " + baseDensity);
- }
- } catch (RemoteException e) {
- }
- return;
- } else if ("reset".equals(densityStr)) {
- density = -1;
- } else {
- try {
- density = Integer.parseInt(densityStr);
- } catch (NumberFormatException e) {
- System.err.println("Error: bad number " + e);
- return;
- }
- if (density < 72) {
- System.err.println("Error: density must be >= 72");
- return;
- }
- }
-
- try {
- if (density > 0) {
- // TODO(multidisplay): For now Configuration only applies to main screen.
- mWm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, density,
- UserHandle.USER_CURRENT);
- } else {
- mWm.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY,
- UserHandle.USER_CURRENT);
- }
- } catch (RemoteException e) {
- }
- }
-
- private void runDisplayOverscan() throws Exception {
- String overscanStr = nextArgRequired();
- Rect rect = new Rect();
- if ("reset".equals(overscanStr)) {
- rect.set(0, 0, 0, 0);
- } else {
- final Pattern FLATTENED_PATTERN = Pattern.compile(
- "(-?\\d+),(-?\\d+),(-?\\d+),(-?\\d+)");
- Matcher matcher = FLATTENED_PATTERN.matcher(overscanStr);
- if (!matcher.matches()) {
- System.err.println("Error: bad rectangle arg: " + overscanStr);
- return;
- }
- rect.left = Integer.parseInt(matcher.group(1));
- rect.top = Integer.parseInt(matcher.group(2));
- rect.right = Integer.parseInt(matcher.group(3));
- rect.bottom = Integer.parseInt(matcher.group(4));
- }
-
- try {
- mWm.setOverscan(Display.DEFAULT_DISPLAY, rect.left, rect.top, rect.right, rect.bottom);
- } catch (RemoteException e) {
- }
- }
-
- private void runDisplayScaling() throws Exception {
- String scalingStr = nextArgRequired();
- if ("auto".equals(scalingStr)) {
- mWm.setForcedDisplayScalingMode(Display.DEFAULT_DISPLAY, 0);
- } else if ("off".equals(scalingStr)) {
- mWm.setForcedDisplayScalingMode(Display.DEFAULT_DISPLAY, 1);
- } else {
- System.err.println("Error: scaling must be 'auto' or 'off'");
- }
- }
-
- private void runDismissKeyguard() throws Exception {
- mWm.dismissKeyguard(null /* callback */);
- }
-
- private int parseDimension(String s) throws NumberFormatException {
- if (s.endsWith("px")) {
- return Integer.parseInt(s.substring(0, s.length() - 2));
- }
- if (s.endsWith("dp")) {
- int density;
- try {
- density = mWm.getBaseDisplayDensity(Display.DEFAULT_DISPLAY);
- } catch (RemoteException e) {
- density = DisplayMetrics.DENSITY_DEFAULT;
- }
- return Integer.parseInt(s.substring(0, s.length() - 2)) * density /
- DisplayMetrics.DENSITY_DEFAULT;
- }
- return Integer.parseInt(s);
- }
-}
diff --git a/com/android/defcontainer/DefaultContainerService.java b/com/android/defcontainer/DefaultContainerService.java
index 4a771ebd..8b01aef8 100644
--- a/com/android/defcontainer/DefaultContainerService.java
+++ b/com/android/defcontainer/DefaultContainerService.java
@@ -138,6 +138,7 @@ public class DefaultContainerService extends IntentService {
ret.packageName = pkg.packageName;
ret.splitNames = pkg.splitNames;
ret.versionCode = pkg.versionCode;
+ ret.versionCodeMajor = pkg.versionCodeMajor;
ret.baseRevisionCode = pkg.baseRevisionCode;
ret.splitRevisionCodes = pkg.splitRevisionCodes;
ret.installLocation = pkg.installLocation;
diff --git a/com/android/ex/photo/ActionBarWrapper.java b/com/android/ex/photo/ActionBarWrapper.java
index 6d4d4d20..ae621979 100644
--- a/com/android/ex/photo/ActionBarWrapper.java
+++ b/com/android/ex/photo/ActionBarWrapper.java
@@ -1,7 +1,8 @@
package com.android.ex.photo;
-import android.app.ActionBar;
+
import android.graphics.drawable.Drawable;
+import android.support.v7.app.ActionBar;
/**
* Wrapper around {@link ActionBar}.
diff --git a/com/android/ex/photo/PhotoViewActivity.java b/com/android/ex/photo/PhotoViewActivity.java
index 7b53918f..a5c4a438 100644
--- a/com/android/ex/photo/PhotoViewActivity.java
+++ b/com/android/ex/photo/PhotoViewActivity.java
@@ -21,14 +21,14 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
-import android.support.v4.app.FragmentActivity;
+import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
/**
* Activity to view the contents of an album.
*/
-public class PhotoViewActivity extends FragmentActivity
+public class PhotoViewActivity extends AppCompatActivity
implements PhotoViewController.ActivityInterface {
private PhotoViewController mController;
@@ -41,7 +41,7 @@ public class PhotoViewActivity extends FragmentActivity
mController.onCreate(savedInstanceState);
}
- public PhotoViewController createController() {
+ protected PhotoViewController createController() {
return new PhotoViewController(this);
}
@@ -122,7 +122,7 @@ public class PhotoViewActivity extends FragmentActivity
@Override
public ActionBarInterface getActionBarInterface() {
if (mActionBar == null) {
- mActionBar = new ActionBarWrapper(getActionBar());
+ mActionBar = new ActionBarWrapper(getSupportActionBar());
}
return mActionBar;
}
diff --git a/com/android/ims/ImsCall.java b/com/android/ims/ImsCall.java
index c5e6368e..6200a07c 100644
--- a/com/android/ims/ImsCall.java
+++ b/com/android/ims/ImsCall.java
@@ -686,7 +686,8 @@ public class ImsCall implements ICall {
*
* @param profile The new call profile.
*/
- private void setCallProfile(ImsCallProfile profile) {
+ @VisibleForTesting
+ public void setCallProfile(ImsCallProfile profile) {
synchronized(mLockObj) {
mCallProfile = profile;
trackVideoStateHistory(mCallProfile);
@@ -2419,6 +2420,12 @@ public class ImsCall implements ICall {
}
}
+ /**
+ * Indicates that an {@link ImsCallSession} has been remotely held. This can be due to the
+ * remote party holding the current call, or swapping between calls.
+ * @param session the session which was held.
+ * @param profile the profile for the held call.
+ */
@Override
public void callSessionHoldReceived(ImsCallSession session, ImsCallProfile profile) {
logi("callSessionHoldReceived :: session=" + session + "profile=" + profile);
@@ -2446,6 +2453,12 @@ public class ImsCall implements ICall {
}
}
+ /**
+ * Indicates that an {@link ImsCallSession} has been remotely resumed. This can be due to
+ * the remote party un-holding the current call, or swapping back to this call.
+ * @param session the session which was resumed.
+ * @param profile the profile for the held call.
+ */
@Override
public void callSessionResumed(ImsCallSession session, ImsCallProfile profile) {
logi("callSessionResumed :: session=" + session + "profile=" + profile);
diff --git a/com/android/ims/ImsConfig.java b/com/android/ims/ImsConfig.java
index cf4c47bf..cd0c4b11 100644
--- a/com/android/ims/ImsConfig.java
+++ b/com/android/ims/ImsConfig.java
@@ -619,6 +619,7 @@ public class ImsConfig {
Rlog.d(TAG, "setProvisionedValue(): item = " + item +
" value = " + value + " ret = " + ret);
}
+
return ret;
}
@@ -647,6 +648,7 @@ public class ImsConfig {
Rlog.d(TAG, "setProvisionedStringValue(): item = " + item +
", value =" + value);
}
+
return ret;
}
diff --git a/com/android/ims/ImsManager.java b/com/android/ims/ImsManager.java
index e0a966a7..e991a5dd 100644
--- a/com/android/ims/ImsManager.java
+++ b/com/android/ims/ImsManager.java
@@ -20,10 +20,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Handler;
import android.os.IBinder;
-import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
import android.os.PersistableBundle;
@@ -50,15 +47,14 @@ import com.android.ims.internal.IImsServiceController;
import com.android.ims.internal.IImsUt;
import com.android.ims.internal.ImsCallSession;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.ExponentialBackoff;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedDeque;
/**
* Provides APIs for IMS services, such as initiating IMS calls, and provides access to
@@ -172,6 +168,11 @@ public class ImsManager {
*/
public static final String EXTRA_IS_UNKNOWN_CALL = "android:isUnknown";
+ private static final int SYSTEM_PROPERTY_NOT_SET = -1;
+
+ // -1 indicates a subscriptionProperty value that is never set.
+ private static final int SUB_PROPERTY_NOT_INITIALIZED = -1;
+
private static final String TAG = "ImsManager";
private static final boolean DBG = true;
@@ -211,14 +212,6 @@ public class ImsManager {
private boolean mHasRegisteredForProxy = false;
private final Object mHasRegisteredLock = new Object();
- // SystemProperties used as cache
- private static final String VOLTE_PROVISIONED_PROP = "net.lte.ims.volte.provisioned";
- private static final String WFC_PROVISIONED_PROP = "net.lte.ims.wfc.provisioned";
- private static final String VT_PROVISIONED_PROP = "net.lte.ims.vt.provisioned";
- // Flag indicating data enabled or not. This flag should be in sync with
- // DcTracker.isDataEnabled(). The flag will be set later during boot up.
- private static final String DATA_ENABLED_PROP = "net.lte.ims.data.enabled";
-
public static final String TRUE = "true";
public static final String FALSE = "false";
@@ -227,19 +220,6 @@ public class ImsManager {
private ConcurrentLinkedDeque<ImsReasonInfo> mRecentDisconnectReasons =
new ConcurrentLinkedDeque<>();
- // Exponential backoff for provisioning cache update. May be null for instances of ImsManager
- // that are not on a thread supporting a looper.
- private ExponentialBackoff mProvisionBackoff;
- // Initial Provisioning check delay in ms
- private static final long BACKOFF_INITIAL_DELAY_MS = 500;
- // Max Provisioning check delay in ms (5 Minutes)
- private static final long BACKOFF_MAX_DELAY_MS = 300000;
- // Multiplier for exponential delay
- private static final int BACKOFF_MULTIPLIER = 2;
- // -1 indicates a subscriptionProperty value that is never set.
- private static final int SUB_PROPERTY_NOT_INITIALIZED = -1;
-
-
/**
* Gets a manager instance.
*
@@ -408,8 +388,13 @@ public class ImsManager {
* basis.
*/
public boolean isVolteEnabledByPlatform() {
- if (SystemProperties.getInt(PROPERTY_DBG_VOLTE_AVAIL_OVERRIDE,
- PROPERTY_DBG_VOLTE_AVAIL_OVERRIDE_DEFAULT) == 1) {
+ // We first read the per slot value. If doesn't exist, we read the general value. If still
+ // doesn't exist, we use the hardcoded default value.
+ if (SystemProperties.getInt(
+ PROPERTY_DBG_VOLTE_AVAIL_OVERRIDE + Integer.toString(mPhoneId),
+ SYSTEM_PROPERTY_NOT_SET) == 1 ||
+ SystemProperties.getInt(PROPERTY_DBG_VOLTE_AVAIL_OVERRIDE,
+ SYSTEM_PROPERTY_NOT_SET) == 1) {
return true;
}
@@ -542,8 +527,12 @@ public class ImsManager {
* which must be done correctly).
*/
public boolean isVtEnabledByPlatform() {
- if (SystemProperties.getInt(PROPERTY_DBG_VT_AVAIL_OVERRIDE,
- PROPERTY_DBG_VT_AVAIL_OVERRIDE_DEFAULT) == 1) {
+ // We first read the per slot value. If doesn't exist, we read the general value. If still
+ // doesn't exist, we use the hardcoded default value.
+ if (SystemProperties.getInt(PROPERTY_DBG_VT_AVAIL_OVERRIDE +
+ Integer.toString(mPhoneId), SYSTEM_PROPERTY_NOT_SET) == 1 ||
+ SystemProperties.getInt(
+ PROPERTY_DBG_VT_AVAIL_OVERRIDE, SYSTEM_PROPERTY_NOT_SET) == 1) {
return true;
}
@@ -647,10 +636,15 @@ public class ImsManager {
* The platform property may override the carrier config.
*/
private boolean isTurnOffImsAllowedByPlatform() {
- if (SystemProperties.getInt(PROPERTY_DBG_ALLOW_IMS_OFF_OVERRIDE,
- PROPERTY_DBG_ALLOW_IMS_OFF_OVERRIDE_DEFAULT) == 1) {
+ // We first read the per slot value. If doesn't exist, we read the general value. If still
+ // doesn't exist, we use the hardcoded default value.
+ if (SystemProperties.getInt(PROPERTY_DBG_ALLOW_IMS_OFF_OVERRIDE +
+ Integer.toString(mPhoneId), SYSTEM_PROPERTY_NOT_SET) == 1 ||
+ SystemProperties.getInt(
+ PROPERTY_DBG_ALLOW_IMS_OFF_OVERRIDE, SYSTEM_PROPERTY_NOT_SET) == 1) {
return true;
}
+
return getBooleanCarrierConfig(
CarrierConfigManager.KEY_CARRIER_ALLOW_TURNOFF_IMS_BOOL);
}
@@ -1036,8 +1030,12 @@ public class ImsManager {
* configuration settings which must be done correctly).
*/
public boolean isWfcEnabledByPlatform() {
- if (SystemProperties.getInt(PROPERTY_DBG_WFC_AVAIL_OVERRIDE,
- PROPERTY_DBG_WFC_AVAIL_OVERRIDE_DEFAULT) == 1) {
+ // We first read the per slot value. If doesn't exist, we read the general value. If still
+ // doesn't exist, we use the hardcoded default value.
+ if (SystemProperties.getInt(PROPERTY_DBG_WFC_AVAIL_OVERRIDE +
+ Integer.toString(mPhoneId), SYSTEM_PROPERTY_NOT_SET) == 1 ||
+ SystemProperties.getInt(
+ PROPERTY_DBG_WFC_AVAIL_OVERRIDE, SYSTEM_PROPERTY_NOT_SET) == 1) {
return true;
}
@@ -1072,116 +1070,28 @@ public class ImsManager {
}
/**
- * This function should be called when ImsConfig.ACTION_IMS_CONFIG_CHANGED is received.
- *
- * We cannot register receiver in ImsManager because this would lead to resource leak.
- * ImsManager can be created in different processes and it is not notified when that process
- * is about to be terminated.
- *
- * @hide
- * */
- public static void onProvisionedValueChanged(Context context, int item, String value) {
- if (DBG) Rlog.d(TAG, "onProvisionedValueChanged: item=" + item + " val=" + value);
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
-
- switch (item) {
- case ImsConfig.ConfigConstants.VLT_SETTING_ENABLED:
- mgr.setVolteProvisionedProperty(value.equals("1"));
- if (DBG) Rlog.d(TAG,"isVoLteProvisioned = " + mgr.isVolteProvisioned());
- break;
-
- case ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED:
- mgr.setWfcProvisionedProperty(value.equals("1"));
- if (DBG) Rlog.d(TAG,"isWfcProvisioned = " + mgr.isWfcProvisioned());
- break;
-
- case ImsConfig.ConfigConstants.LVC_SETTING_ENABLED:
- mgr.setVtProvisionedProperty(value.equals("1"));
- if (DBG) Rlog.d(TAG,"isVtProvisioned = " + mgr.isVtProvisioned());
- break;
-
- }
- }
-
- private class AsyncUpdateProvisionedValues extends AsyncTask<Void, Void, Boolean> {
- @Override
- protected Boolean doInBackground(Void... params) {
- // disable on any error
- setVolteProvisionedProperty(false);
- setWfcProvisionedProperty(false);
- setVtProvisionedProperty(false);
-
- try {
- ImsConfig config = getConfigInterface();
- if (config != null) {
- setVolteProvisionedProperty(getProvisionedBool(config,
- ImsConfig.ConfigConstants.VLT_SETTING_ENABLED));
- if (DBG) Rlog.d(TAG, "isVoLteProvisioned = " + isVolteProvisioned());
-
- setWfcProvisionedProperty(getProvisionedBool(config,
- ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
- if (DBG) Rlog.d(TAG, "isWfcProvisioned = " + isWfcProvisioned());
-
- setVtProvisionedProperty(getProvisionedBool(config,
- ImsConfig.ConfigConstants.LVC_SETTING_ENABLED));
- if (DBG) Rlog.d(TAG, "isVtProvisioned = " + isVtProvisioned());
-
- }
- } catch (ImsException ie) {
- Rlog.e(TAG, "AsyncUpdateProvisionedValues error: ", ie);
- return false;
- }
-
- return true;
- }
-
- @Override
- protected void onPostExecute(Boolean completed) {
- if (mProvisionBackoff == null) {
- return;
- }
- if (!completed) {
- mProvisionBackoff.notifyFailed();
- } else {
- mProvisionBackoff.stop();
- }
- }
-
- /**
- * Will return with config value or throw an ImsException if we receive an error from
- * ImsConfig for that value.
- */
- private boolean getProvisionedBool(ImsConfig config, int item) throws ImsException {
- int value = config.getProvisionedValue(item);
- if (value == ImsConfig.FeatureValueConstants.ERROR) {
- throw new ImsException("getProvisionedBool failed with error for item: " + item,
- ImsReasonInfo.CODE_LOCAL_INTERNAL_ERROR);
- }
- return config.getProvisionedValue(item) == ImsConfig.FeatureValueConstants.ON;
- }
- }
-
- // used internally only, use #updateProvisionedValues instead.
- private void handleUpdateProvisionedValues() {
- if (getBooleanCarrierConfig(
- CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL)) {
-
- new AsyncUpdateProvisionedValues().execute();
+ * Will return with config value or throw an ImsException if we receive an error from
+ * ImsConfig for that value.
+ */
+ private boolean getProvisionedBool(ImsConfig config, int item) throws ImsException {
+ int value = config.getProvisionedValue(item);
+ if (value == ImsConfig.OperationStatusConstants.UNKNOWN) {
+ throw new ImsException("getProvisionedBool failed with error for item: " + item,
+ ImsReasonInfo.CODE_LOCAL_INTERNAL_ERROR);
}
+ return config.getProvisionedValue(item) == ImsConfig.FeatureValueConstants.ON;
}
/**
- * Asynchronously get VoLTE, WFC, VT provisioning statuses. If ImsConfig is not available, we
- * will retry with exponential backoff.
+ * Will return with config value or return false if we receive an error from
+ * ImsConfig for that value.
*/
- private void updateProvisionedValues() {
- // Start trying to receive provisioning status after BACKOFF_INITIAL_DELAY_MS.
- if (mProvisionBackoff != null) {
- mProvisionBackoff.start();
- } else {
- // bypass and launch async thread once without backoff.
- handleUpdateProvisionedValues();
+ private boolean getProvisionedBoolNoException(int item) {
+ try {
+ ImsConfig config = getConfigInterface();
+ return getProvisionedBool(config, item);
+ } catch (ImsException ex) {
+ return false;
}
}
@@ -1206,8 +1116,6 @@ public class ImsManager {
/**
* Sync carrier config and user settings with ImsConfig.
*
- * @param context for the manager object
- * @param phoneId phone id
* @param force update
*/
public void updateImsServiceConfig(boolean force) {
@@ -1222,8 +1130,6 @@ public class ImsManager {
if (!mConfigUpdated || force) {
try {
- updateProvisionedValues();
-
// TODO: Extend ImsConfig API and set all feature values in single function call.
// Note: currently the order of updates is set to produce different order of
@@ -1361,11 +1267,6 @@ public class ImsManager {
com.android.internal.R.bool.config_dynamic_bind_ims);
mConfigManager = (CarrierConfigManager) context.getSystemService(
Context.CARRIER_CONFIG_SERVICE);
- if (Looper.getMainLooper() != null) {
- mProvisionBackoff = new ExponentialBackoff(BACKOFF_INITIAL_DELAY_MS,
- BACKOFF_MAX_DELAY_MS, BACKOFF_MULTIPLIER,
- new Handler(Looper.getMainLooper()), this::handleUpdateProvisionedValues);
- }
createImsService();
}
@@ -2449,40 +2350,22 @@ public class ImsManager {
}
private boolean isDataEnabled() {
- return SystemProperties.getBoolean(DATA_ENABLED_PROP, true);
- }
-
- /**
- * Set data enabled/disabled flag.
- * @param enabled True if data is enabled, otherwise disabled.
- */
- public void setDataEnabled(boolean enabled) {
- log("setDataEnabled: " + enabled);
- SystemProperties.set(DATA_ENABLED_PROP, enabled ? TRUE : FALSE);
+ return new TelephonyManager(mContext, getSubId()).isMobileDataEnabled();
}
private boolean isVolteProvisioned() {
- return SystemProperties.getBoolean(VOLTE_PROVISIONED_PROP, true);
- }
-
- private void setVolteProvisionedProperty(boolean provisioned) {
- SystemProperties.set(VOLTE_PROVISIONED_PROP, provisioned ? TRUE : FALSE);
+ return getProvisionedBoolNoException(
+ ImsConfig.ConfigConstants.VLT_SETTING_ENABLED);
}
private boolean isWfcProvisioned() {
- return SystemProperties.getBoolean(WFC_PROVISIONED_PROP, true);
- }
-
- private void setWfcProvisionedProperty(boolean provisioned) {
- SystemProperties.set(WFC_PROVISIONED_PROP, provisioned ? TRUE : FALSE);
+ return getProvisionedBoolNoException(
+ ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED);
}
private boolean isVtProvisioned() {
- return SystemProperties.getBoolean(VT_PROVISIONED_PROP, true);
- }
-
- private void setVtProvisionedProperty(boolean provisioned) {
- SystemProperties.set(VT_PROVISIONED_PROP, provisioned ? TRUE : FALSE);
+ return getProvisionedBoolNoException(
+ ImsConfig.ConfigConstants.LVC_SETTING_ENABLED);
}
private static String booleanToPropertyString(boolean bool) {
diff --git a/com/android/ims/ImsReasonInfo.java b/com/android/ims/ImsReasonInfo.java
index cdfc1fd8..4f6f68c3 100644
--- a/com/android/ims/ImsReasonInfo.java
+++ b/com/android/ims/ImsReasonInfo.java
@@ -104,6 +104,9 @@ public class ImsReasonInfo implements Parcelable {
// MT : No action from user after alerting the call
public static final int CODE_TIMEOUT_NO_ANSWER_CALL_UPDATE = 203;
+ //Call was blocked by call barring
+ public static final int CODE_CALL_BARRED = 240;
+
//Call failures for FDN
public static final int CODE_FDN_BLOCKED = 241;
@@ -111,6 +114,16 @@ public class ImsReasonInfo implements Parcelable {
// and this capability is not supported by the network.
public static final int CODE_IMEI_NOT_ACCEPTED = 243;
+ //STK CC errors
+ public static final int CODE_DIAL_MODIFIED_TO_USSD = 244;
+ public static final int CODE_DIAL_MODIFIED_TO_SS = 245;
+ public static final int CODE_DIAL_MODIFIED_TO_DIAL = 246;
+ public static final int CODE_DIAL_MODIFIED_TO_DIAL_VIDEO = 247;
+ public static final int CODE_DIAL_VIDEO_MODIFIED_TO_DIAL = 248;
+ public static final int CODE_DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO = 249;
+ public static final int CODE_DIAL_VIDEO_MODIFIED_TO_SS = 250;
+ public static final int CODE_DIAL_VIDEO_MODIFIED_TO_USSD = 251;
+
/**
* STATUSCODE (SIP response code) (IMS -> Telephony)
*/
@@ -217,6 +230,11 @@ public class ImsReasonInfo implements Parcelable {
public static final int CODE_UT_OPERATION_NOT_ALLOWED = 803;
public static final int CODE_UT_NETWORK_ERROR = 804;
public static final int CODE_UT_CB_PASSWORD_MISMATCH = 821;
+ //STK CC errors
+ public static final int CODE_UT_SS_MODIFIED_TO_DIAL = 822;
+ public static final int CODE_UT_SS_MODIFIED_TO_USSD = 823;
+ public static final int CODE_UT_SS_MODIFIED_TO_SS = 824;
+ public static final int CODE_UT_SS_MODIFIED_TO_DIAL_VIDEO = 825;
/**
* ECBM
diff --git a/com/android/ims/ImsSsData.java b/com/android/ims/ImsSsData.java
new file mode 100644
index 00000000..7336c133
--- /dev/null
+++ b/com/android/ims/ImsSsData.java
@@ -0,0 +1,189 @@
+/*
+ * 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 com.android.ims;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+
+/**
+ * Provided STK Call Control Suplementary Service information
+ *
+ * {@hide}
+ */
+public class ImsSsData implements Parcelable {
+
+ //ServiceType
+ public static final int SS_CFU = 0;
+ public static final int SS_CF_BUSY = 1;
+ public static final int SS_CF_NO_REPLY = 2;
+ public static final int SS_CF_NOT_REACHABLE = 3;
+ public static final int SS_CF_ALL = 4;
+ public static final int SS_CF_ALL_CONDITIONAL = 5;
+ public static final int SS_CFUT = 6;
+ public static final int SS_CLIP = 7;
+ public static final int SS_CLIR = 8;
+ public static final int SS_COLP = 9;
+ public static final int SS_COLR = 10;
+ public static final int SS_CNAP = 11;
+ public static final int SS_WAIT = 12;
+ public static final int SS_BAOC = 13;
+ public static final int SS_BAOIC = 14;
+ public static final int SS_BAOIC_EXC_HOME = 15;
+ public static final int SS_BAIC = 16;
+ public static final int SS_BAIC_ROAMING = 17;
+ public static final int SS_ALL_BARRING = 18;
+ public static final int SS_OUTGOING_BARRING = 19;
+ public static final int SS_INCOMING_BARRING = 20;
+ public static final int SS_INCOMING_BARRING_DN = 21;
+ public static final int SS_INCOMING_BARRING_ANONYMOUS = 22;
+
+ //SSRequestType
+ public static final int SS_ACTIVATION = 0;
+ public static final int SS_DEACTIVATION = 1;
+ public static final int SS_INTERROGATION = 2;
+ public static final int SS_REGISTRATION = 3;
+ public static final int SS_ERASURE = 4;
+
+ //TeleserviceType
+ public static final int SS_ALL_TELE_AND_BEARER_SERVICES = 0;
+ public static final int SS_ALL_TELESEVICES = 1;
+ public static final int SS_TELEPHONY = 2;
+ public static final int SS_ALL_DATA_TELESERVICES = 3;
+ public static final int SS_SMS_SERVICES = 4;
+ public static final int SS_ALL_TELESERVICES_EXCEPT_SMS = 5;
+
+ // Refer to ServiceType
+ public int serviceType;
+ // Refere to SSRequestType
+ public int requestType;
+ // Refer to TeleserviceType
+ public int teleserviceType;
+ // Service Class
+ public int serviceClass;
+ // Error information
+ public int result;
+
+ public int[] ssInfo; /* Valid for all supplementary services.
+ This field will be empty for RequestType SS_INTERROGATION
+ and ServiceType SS_CF_*, SS_INCOMING_BARRING_DN,
+ SS_INCOMING_BARRING_ANONYMOUS.*/
+
+ public ImsCallForwardInfo[] cfInfo; /* Valid only for supplementary services
+ ServiceType SS_CF_* and RequestType SS_INTERROGATION */
+
+ public ImsSsInfo[] imsSsInfo; /* Valid only for ServiceType SS_INCOMING_BARRING_DN and
+ ServiceType SS_INCOMING_BARRING_ANONYMOUS */
+
+ public ImsSsData() {}
+
+ public ImsSsData(Parcel in) {
+ readFromParcel(in);
+ }
+
+ public static final Creator<ImsSsData> CREATOR = new Creator<ImsSsData>() {
+ @Override
+ public ImsSsData createFromParcel(Parcel in) {
+ return new ImsSsData(in);
+ }
+
+ @Override
+ public ImsSsData[] newArray(int size) {
+ return new ImsSsData[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(serviceType);
+ out.writeInt(requestType);
+ out.writeInt(teleserviceType);
+ out.writeInt(serviceClass);
+ out.writeInt(result);
+ out.writeIntArray(ssInfo);
+ out.writeParcelableArray(cfInfo, 0);
+ }
+
+ private void readFromParcel(Parcel in) {
+ serviceType = in.readInt();
+ requestType = in.readInt();
+ teleserviceType = in.readInt();
+ serviceClass = in.readInt();
+ result = in.readInt();
+ ssInfo = in.createIntArray();
+ cfInfo = (ImsCallForwardInfo[])in.readParcelableArray(this.getClass().getClassLoader());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public boolean isTypeCF() {
+ return (serviceType == SS_CFU || serviceType == SS_CF_BUSY ||
+ serviceType == SS_CF_NO_REPLY || serviceType == SS_CF_NOT_REACHABLE ||
+ serviceType == SS_CF_ALL || serviceType == SS_CF_ALL_CONDITIONAL);
+ }
+
+ public boolean isTypeUnConditional() {
+ return (serviceType == SS_CFU || serviceType == SS_CF_ALL);
+ }
+
+ public boolean isTypeCW() {
+ return (serviceType == SS_WAIT);
+ }
+
+ public boolean isTypeClip() {
+ return (serviceType == SS_CLIP);
+ }
+
+ public boolean isTypeColr() {
+ return (serviceType == SS_COLR);
+ }
+
+ public boolean isTypeColp() {
+ return (serviceType == SS_COLP);
+ }
+
+ public boolean isTypeClir() {
+ return (serviceType == SS_CLIR);
+ }
+
+ public boolean isTypeIcb() {
+ return (serviceType == SS_INCOMING_BARRING_DN ||
+ serviceType == SS_INCOMING_BARRING_ANONYMOUS);
+ }
+
+ public boolean isTypeBarring() {
+ return (serviceType == SS_BAOC || serviceType == SS_BAOIC ||
+ serviceType == SS_BAOIC_EXC_HOME || serviceType == SS_BAIC ||
+ serviceType == SS_BAIC_ROAMING || serviceType == SS_ALL_BARRING ||
+ serviceType == SS_OUTGOING_BARRING || serviceType == SS_INCOMING_BARRING);
+ }
+
+ public boolean isTypeInterrogation() {
+ return (requestType == SS_INTERROGATION);
+ }
+
+ public String toString() {
+ return "[ImsSsData] " + "ServiceType: " + serviceType
+ + " RequestType: " + requestType
+ + " TeleserviceType: " + teleserviceType
+ + " ServiceClass: " + serviceClass
+ + " Result: " + result;
+ }
+}
diff --git a/com/android/ims/ImsUt.java b/com/android/ims/ImsUt.java
index d8d70b05..eaeb5511 100644
--- a/com/android/ims/ImsUt.java
+++ b/com/android/ims/ImsUt.java
@@ -22,7 +22,9 @@ import java.util.Map;
import android.content.res.Resources;
import android.os.AsyncResult;
import android.os.Bundle;
+import android.os.Handler;
import android.os.Message;
+import android.os.Registrant;
import android.os.RemoteException;
import android.telephony.Rlog;
@@ -67,11 +69,16 @@ public class ImsUt implements ImsUtInterface {
private static final String TAG = "ImsUt";
private static final boolean DBG = true;
+ //These service class values are same as the one in CommandsInterface.java
+ private static final int SERVICE_CLASS_NONE = 0;
+ private static final int SERVICE_CLASS_VOICE = (1 << 0);
+
// For synchronization of private variables
private Object mLockObj = new Object();
private final IImsUt miUt;
private HashMap<Integer, Message> mPendingCmds =
new HashMap<Integer, Message>();
+ private Registrant mSsIndicationRegistrant;
public ImsUt(IImsUt iUt) {
miUt = iUt;
@@ -108,6 +115,23 @@ public class ImsUt implements ImsUtInterface {
}
/**
+ * Registers a handler for Supplementary Service Indications. The
+ * result is returned in the {@link AsyncResult#result) field
+ * of the {@link AsyncResult} object returned by {@link Message.obj}.
+ * Value of ((AsyncResult)result.obj) is of {@link ImsSsData}.
+ */
+ public void registerForSuppServiceIndication(Handler h, int what, Object obj) {
+ mSsIndicationRegistrant = new Registrant (h, what, obj);
+ }
+
+ /**
+ * UnRegisters a handler for Supplementary Service Indications.
+ */
+ public void unregisterForSuppServiceIndication(Handler h) {
+ mSsIndicationRegistrant.clear();
+ }
+
+ /**
* Operations for the supplementary service configuration
*/
@@ -117,16 +141,31 @@ public class ImsUt implements ImsUtInterface {
* @param cbType type of call barring to be queried; ImsUtInterface#CB_XXX
* @param result message to pass the result of this operation
* The return value of ((AsyncResult)result.obj) is an array of {@link ImsSsInfo}.
+ * @deprecated Use {@link #queryCallBarring(int, Message, int)} instead.
*/
@Override
public void queryCallBarring(int cbType, Message result) {
+ queryCallBarring(cbType, result, SERVICE_CLASS_NONE);
+ }
+
+ /**
+ * Retrieves the configuration of the call barring for specified service class.
+ *
+ * @param cbType type of call barring to be queried; ImsUtInterface#CB_XXX
+ * @param result message to pass the result of this operation
+ * The return value of ((AsyncResult)result.obj) is an array of {@link ImsSsInfo}.
+ * @param serviceClass service class for e.g. voice/video
+ */
+ @Override
+ public void queryCallBarring(int cbType, Message result, int serviceClass) {
if (DBG) {
- log("queryCallBarring :: Ut=" + miUt + ", cbType=" + cbType);
+ log("queryCallBarring :: Ut=" + miUt + ", cbType=" + cbType + ", serviceClass="
+ + serviceClass);
}
synchronized(mLockObj) {
try {
- int id = miUt.queryCallBarring(cbType);
+ int id = miUt.queryCallBarringForServiceClass(cbType, serviceClass);
if (id < 0) {
sendFailureReport(result,
@@ -306,9 +345,19 @@ public class ImsUt implements ImsUtInterface {
/**
* Modifies the configuration of the call barring.
+ * @deprecated Use {@link #updateCallBarring(int, int, Message, String[], int)} instead.
*/
@Override
public void updateCallBarring(int cbType, int action, Message result, String[] barrList) {
+ updateCallBarring(cbType, action, result, barrList, SERVICE_CLASS_NONE);
+ }
+
+ /**
+ * Modifies the configuration of the call barring for specified service class.
+ */
+ @Override
+ public void updateCallBarring(int cbType, int action, Message result,
+ String[] barrList, int serviceClass) {
if (DBG) {
if (barrList != null) {
String bList = new String();
@@ -316,17 +365,19 @@ public class ImsUt implements ImsUtInterface {
bList.concat(barrList[i] + " ");
}
log("updateCallBarring :: Ut=" + miUt + ", cbType=" + cbType
- + ", action=" + action + ", barrList=" + bList);
+ + ", action=" + action + ", serviceClass=" + serviceClass
+ + ", barrList=" + bList);
}
else {
log("updateCallBarring :: Ut=" + miUt + ", cbType=" + cbType
- + ", action=" + action);
+ + ", action=" + action + ", serviceClass=" + serviceClass);
}
}
synchronized(mLockObj) {
try {
- int id = miUt.updateCallBarring(cbType, action, barrList);
+ int id = miUt.updateCallBarringForServiceClass(cbType, action,
+ barrList, serviceClass);
if (id < 0) {
sendFailureReport(result,
@@ -678,5 +729,15 @@ public class ImsUt implements ImsUtInterface {
mPendingCmds.remove(key);
}
}
+
+ /**
+ * Notifies client when Supplementary Service indication is received
+ */
+ @Override
+ public void onSupplementaryServiceIndication(ImsSsData ssData) {
+ if (mSsIndicationRegistrant != null) {
+ mSsIndicationRegistrant.notifyResult(ssData);
+ }
+ }
}
}
diff --git a/com/android/ims/ImsUtInterface.java b/com/android/ims/ImsUtInterface.java
index 5984e789..14c184a6 100644
--- a/com/android/ims/ImsUtInterface.java
+++ b/com/android/ims/ImsUtInterface.java
@@ -16,6 +16,7 @@
package com.android.ims;
+import android.os.Handler;
import android.os.Message;
/**
@@ -109,6 +110,12 @@ public interface ImsUtInterface {
public void queryCallBarring(int cbType, Message result);
/**
+ * Retrieves the configuration of the call barring for specified service class.
+ * The return value of ((AsyncResult)result.obj) is an array of {@link ImsSsInfo}.
+ */
+ public void queryCallBarring(int cbType, Message result, int serviceClass);
+
+ /**
* Retrieves the configuration of the call forward.
* The return value of ((AsyncResult)result.obj) is an array of {@link ImsCallForwardInfo}.
*/
@@ -147,6 +154,12 @@ public interface ImsUtInterface {
Message result, String[] barrList);
/**
+ * Modifies the configuration of the call barring for specified service class.
+ */
+ public void updateCallBarring(int cbType, int action, Message result,
+ String[] barrList, int serviceClass);
+
+ /**
* Modifies the configuration of the call forward.
*/
public void updateCallForward(int action, int condition, String number,
@@ -176,4 +189,18 @@ public interface ImsUtInterface {
* Updates the configuration of the COLP supplementary service.
*/
public void updateCOLP(boolean enable, Message result);
+
+ /**
+ * Register for UNSOL_ON_SS indications.
+ * @param handler the {@link Handler} that is notified when there is an ss indication.
+ * @param event Supplimentary service indication event.
+ * @param Object user object.
+ */
+ public void registerForSuppServiceIndication(Handler handler, int event, Object object);
+
+ /**
+ * Deregister for UNSOL_ON_SS indications.
+ * @param handler the {@link Handler} that is notified when there is an ss indication.
+ */
+ public void unregisterForSuppServiceIndication(Handler handler);
}
diff --git a/com/android/server/policy/AccessibilityShortcutController.java b/com/android/internal/accessibility/AccessibilityShortcutController.java
index 55c582ed..293471c6 100644
--- a/com/android/server/policy/AccessibilityShortcutController.java
+++ b/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012 Google Inc.
+ * Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
@@ -14,7 +14,7 @@
* the License.
*/
-package com.android.server.policy;
+package com.android.internal.accessibility;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.ActivityManager;
@@ -35,6 +35,7 @@ import android.os.UserHandle;
import android.os.Vibrator;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Slog;
import android.view.Window;
import android.view.WindowManager;
@@ -43,20 +44,30 @@ import android.view.accessibility.AccessibilityManager;
import android.widget.Toast;
import com.android.internal.R;
-import java.util.List;
+import java.util.Collections;
+import java.util.Map;
import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
+import static com.android.internal.util.ArrayUtils.convertToLongArray;
+
/**
* Class to help manage the accessibility shortcut
*/
public class AccessibilityShortcutController {
private static final String TAG = "AccessibilityShortcutController";
+
+ // Dummy component names for framework features
+ public static final ComponentName COLOR_INVERSION_COMPONENT_NAME =
+ new ComponentName("com.android.server.accessibility", "ColorInversion");
+ public static final ComponentName DALTONIZER_COMPONENT_NAME =
+ new ComponentName("com.android.server.accessibility", "Daltonizer");
+
private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
.build();
-
+ private static Map<ComponentName, ToggleableFrameworkFeatureInfo> sFrameworkShortcutFeaturesMap;
private final Context mContext;
private AlertDialog mAlertDialog;
@@ -67,6 +78,15 @@ public class AccessibilityShortcutController {
// Visible for testing
public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
+ /**
+ * Get the component name string for the service or feature currently assigned to the
+ * accessiblity shortcut
+ *
+ * @param context A valid context
+ * @param userId The user ID of interest
+ * @return The flattened component name string of the service selected by the user, or the
+ * string for the default service if the user has not made a selection
+ */
public static String getTargetServiceComponentNameString(
Context context, int userId) {
final String currentShortcutServiceId = Settings.Secure.getStringForUser(
@@ -78,6 +98,29 @@ public class AccessibilityShortcutController {
return context.getString(R.string.config_defaultAccessibilityService);
}
+ /**
+ * @return An immutable map from dummy component names to feature info for toggling a framework
+ * feature
+ */
+ public static Map<ComponentName, ToggleableFrameworkFeatureInfo>
+ getFrameworkShortcutFeaturesMap() {
+ if (sFrameworkShortcutFeaturesMap == null) {
+ Map<ComponentName, ToggleableFrameworkFeatureInfo> featuresMap = new ArrayMap<>(2);
+ featuresMap.put(COLOR_INVERSION_COMPONENT_NAME,
+ new ToggleableFrameworkFeatureInfo(
+ Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
+ "1" /* Value to enable */, "0" /* Value to disable */,
+ R.string.color_inversion_feature_name));
+ featuresMap.put(DALTONIZER_COMPONENT_NAME,
+ new ToggleableFrameworkFeatureInfo(
+ Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
+ "1" /* Value to enable */, "0" /* Value to disable */,
+ R.string.color_correction_feature_name));
+ sFrameworkShortcutFeaturesMap = Collections.unmodifiableMap(featuresMap);
+ }
+ return sFrameworkShortcutFeaturesMap;
+ }
+
public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) {
mContext = context;
mUserId = initialUserId;
@@ -160,8 +203,8 @@ public class AccessibilityShortcutController {
if ((vibrator != null) && vibrator.hasVibrator()) {
// Don't check if haptics are disabled, as we need to alert the user that their
// way of interacting with the phone may change if they activate the shortcut
- long[] vibePattern = PhoneWindowManager.getLongIntArray(mContext.getResources(),
- R.array.config_longPressVibePattern);
+ long[] vibePattern = convertToLongArray(
+ mContext.getResources().getIntArray(R.array.config_longPressVibePattern));
vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
}
@@ -187,22 +230,24 @@ public class AccessibilityShortcutController {
}
// Show a toast alerting the user to what's happening
- final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
- if (serviceInfo == null) {
+ final String serviceName = getShortcutFeatureDescription(false /* no summary */);
+ if (serviceName == null) {
Slog.e(TAG, "Accessibility shortcut set to invalid service");
return;
}
- String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
- ? R.string.accessibility_shortcut_disabling_service
- : R.string.accessibility_shortcut_enabling_service);
- String toastMessage = String.format(toastMessageFormatString,
- serviceInfo.getResolveInfo()
- .loadLabel(mContext.getPackageManager()).toString());
- Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
- mContext, toastMessage, Toast.LENGTH_LONG);
- warningToast.getWindowParams().privateFlags |=
- WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
- warningToast.show();
+ // For accessibility services, show a toast explaining what we're doing.
+ final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
+ if (serviceInfo != null) {
+ String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
+ ? R.string.accessibility_shortcut_disabling_service
+ : R.string.accessibility_shortcut_enabling_service);
+ String toastMessage = String.format(toastMessageFormatString, serviceName);
+ Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
+ mContext, toastMessage, Toast.LENGTH_LONG);
+ warningToast.getWindowParams().privateFlags |=
+ WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+ warningToast.show();
+ }
mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
.performAccessibilityShortcut();
@@ -210,18 +255,18 @@ public class AccessibilityShortcutController {
}
private AlertDialog createShortcutWarningDialog(int userId) {
- final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
+ final String serviceDescription = getShortcutFeatureDescription(true /* Include summary */);
- if (serviceInfo == null) {
+ if (serviceDescription == null) {
return null;
}
final String warningMessage = String.format(
mContext.getString(R.string.accessibility_shortcut_toogle_warning),
- serviceInfo.getResolveInfo().loadLabel(mContext.getPackageManager()).toString());
+ serviceDescription);
final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(
// Use SystemUI context so we pick up any theme set in a vendor overlay
- ActivityThread.currentActivityThread().getSystemUiContext())
+ mFrameworkObjectProvider.getSystemUiContext())
.setTitle(R.string.accessibility_shortcut_warning_dialog_title)
.setMessage(warningMessage)
.setCancelable(false)
@@ -253,6 +298,34 @@ public class AccessibilityShortcutController {
ComponentName.unflattenFromString(currentShortcutServiceString));
}
+ private String getShortcutFeatureDescription(boolean includeSummary) {
+ final String currentShortcutServiceString = getTargetServiceComponentNameString(
+ mContext, UserHandle.USER_CURRENT);
+ if (currentShortcutServiceString == null) {
+ return null;
+ }
+ final ComponentName targetComponentName =
+ ComponentName.unflattenFromString(currentShortcutServiceString);
+ final ToggleableFrameworkFeatureInfo frameworkFeatureInfo =
+ getFrameworkShortcutFeaturesMap().get(targetComponentName);
+ if (frameworkFeatureInfo != null) {
+ return frameworkFeatureInfo.getLabel(mContext);
+ }
+ final AccessibilityServiceInfo serviceInfo = mFrameworkObjectProvider
+ .getAccessibilityManagerInstance(mContext).getInstalledServiceInfoWithComponentName(
+ targetComponentName);
+ if (serviceInfo == null) {
+ return null;
+ }
+ final PackageManager pm = mContext.getPackageManager();
+ String label = serviceInfo.getResolveInfo().loadLabel(pm).toString();
+ String summary = serviceInfo.loadSummary(pm).toString();
+ if (!includeSummary || TextUtils.isEmpty(summary)) {
+ return label;
+ }
+ return String.format("%s\n%s", label, summary);
+ }
+
private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
AccessibilityManager accessibilityManager =
mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
@@ -264,6 +337,51 @@ public class AccessibilityShortcutController {
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
+ /**
+ * Immutable class to hold info about framework features that can be controlled by shortcut
+ */
+ public static class ToggleableFrameworkFeatureInfo {
+ private final String mSettingKey;
+ private final String mSettingOnValue;
+ private final String mSettingOffValue;
+ private final int mLabelStringResourceId;
+ // These go to the settings wrapper
+ private int mIconDrawableId;
+
+ ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue,
+ String settingOffValue, int labelStringResourceId) {
+ mSettingKey = settingKey;
+ mSettingOnValue = settingOnValue;
+ mSettingOffValue = settingOffValue;
+ mLabelStringResourceId = labelStringResourceId;
+ }
+
+ /**
+ * @return The settings key to toggle between two values
+ */
+ public String getSettingKey() {
+ return mSettingKey;
+ }
+
+ /**
+ * @return The value to write to settings to turn the feature on
+ */
+ public String getSettingOnValue() {
+ return mSettingOnValue;
+ }
+
+ /**
+ * @return The value to write to settings to turn the feature off
+ */
+ public String getSettingOffValue() {
+ return mSettingOffValue;
+ }
+
+ public String getLabel(Context context) {
+ return context.getString(mLabelStringResourceId);
+ }
+ }
+
// Class to allow mocking of static framework calls
public static class FrameworkObjectProvider {
public AccessibilityManager getAccessibilityManagerInstance(Context context) {
@@ -277,5 +395,9 @@ public class AccessibilityShortcutController {
public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
return Toast.makeText(context, charSequence, duration);
}
+
+ public Context getSystemUiContext() {
+ return ActivityThread.currentActivityThread().getSystemUiContext();
+ }
}
}
diff --git a/com/android/internal/app/SuggestedLocaleAdapter.java b/com/android/internal/app/SuggestedLocaleAdapter.java
index 46f47a31..d1382415 100644
--- a/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -199,7 +199,7 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
text.setTextLocale(item.getLocale());
text.setContentDescription(item.getContentDescription(mCountryMode));
if (mCountryMode) {
- int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent());
+ int layoutDir = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault());
//noinspection ResourceType
convertView.setLayoutDirection(layoutDir);
text.setTextDirection(layoutDir == View.LAYOUT_DIRECTION_RTL
diff --git a/com/android/internal/app/UnlaunchableAppActivity.java b/com/android/internal/app/UnlaunchableAppActivity.java
index 0a539f19..2eadaf3a 100644
--- a/com/android/internal/app/UnlaunchableAppActivity.java
+++ b/com/android/internal/app/UnlaunchableAppActivity.java
@@ -111,14 +111,7 @@ public class UnlaunchableAppActivity extends Activity
@Override
public void onClick(DialogInterface dialog, int which) {
if (mReason == UNLAUNCHABLE_REASON_QUIET_MODE && which == DialogInterface.BUTTON_POSITIVE) {
- if (UserManager.get(this).trySetQuietModeDisabled(mUserId, mTarget)
- && mTarget != null) {
- try {
- startIntentSenderForResult(mTarget, -1, null, 0, 0, 0);
- } catch (IntentSender.SendIntentException e) {
- /* ignore */
- }
- }
+ UserManager.get(this).trySetQuietModeEnabled(false, UserHandle.of(mUserId), mTarget);
}
}
diff --git a/com/android/internal/app/procstats/ProcessState.java b/com/android/internal/app/procstats/ProcessState.java
index fbdf17d8..efc9c02f 100644
--- a/com/android/internal/app/procstats/ProcessState.java
+++ b/com/android/internal/app/procstats/ProcessState.java
@@ -28,6 +28,7 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DebugUtils;
import android.util.Log;
+import android.util.LongSparseArray;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
@@ -90,14 +91,14 @@ public final class ProcessState {
STATE_TOP, // ActivityManager.PROCESS_STATE_TOP
STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
- STATE_TOP, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
STATE_IMPORTANT_BACKGROUND, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
STATE_IMPORTANT_BACKGROUND, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
STATE_BACKUP, // ActivityManager.PROCESS_STATE_BACKUP
- STATE_HEAVY_WEIGHT, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
STATE_SERVICE, // ActivityManager.PROCESS_STATE_SERVICE
STATE_RECEIVER, // ActivityManager.PROCESS_STATE_RECEIVER
+ STATE_TOP, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+ STATE_HEAVY_WEIGHT, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
STATE_HOME, // ActivityManager.PROCESS_STATE_HOME
STATE_LAST_ACTIVITY, // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
STATE_CACHED_ACTIVITY, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
@@ -137,7 +138,7 @@ public final class ProcessState {
private final String mName;
private final String mPackage;
private final int mUid;
- private final int mVersion;
+ private final long mVersion;
private final DurationsTable mDurations;
private final PssTable mPssTable;
@@ -170,7 +171,7 @@ public final class ProcessState {
* Create a new top-level process state, for the initial case where there is only
* a single package running in a process. The initial state is not running.
*/
- public ProcessState(ProcessStats processStats, String pkg, int uid, int vers, String name) {
+ public ProcessState(ProcessStats processStats, String pkg, int uid, long vers, String name) {
mStats = processStats;
mName = name;
mCommonProcess = this;
@@ -186,7 +187,7 @@ public final class ProcessState {
* state. The current running state of the top-level process is also copied,
* marked as started running at 'now'.
*/
- public ProcessState(ProcessState commonProcess, String pkg, int uid, int vers, String name,
+ public ProcessState(ProcessState commonProcess, String pkg, int uid, long vers, String name,
long now) {
mStats = commonProcess.mStats;
mName = name;
@@ -238,7 +239,7 @@ public final class ProcessState {
return mUid;
}
- public int getVersion() {
+ public long getVersion() {
return mVersion;
}
@@ -546,7 +547,7 @@ public final class ProcessState {
// The array map is still pointing to a common process state
// that is now shared across packages. Update it to point to
// the new per-package state.
- SparseArray<PackageState> vpkg = mStats.mPackages.get(pkgName, mUid);
+ LongSparseArray<PackageState> vpkg = mStats.mPackages.get(pkgName, mUid);
if (vpkg == null) {
throw new IllegalStateException("Didn't find package " + pkgName
+ " / " + mUid);
@@ -584,7 +585,7 @@ public final class ProcessState {
// The array map is still pointing to a common process state
// that is now shared across packages. Update it to point to
// the new per-package state.
- SparseArray<PackageState> vpkg = mStats.mPackages.get(pkgList.keyAt(index),
+ LongSparseArray<PackageState> vpkg = mStats.mPackages.get(pkgList.keyAt(index),
proc.mUid);
if (vpkg == null) {
throw new IllegalStateException("No existing package "
@@ -1037,7 +1038,7 @@ public final class ProcessState {
}
}
- public void dumpPackageProcCheckin(PrintWriter pw, String pkgName, int uid, int vers,
+ public void dumpPackageProcCheckin(PrintWriter pw, String pkgName, int uid, long vers,
String itemName, long now) {
pw.print("pkgproc,");
pw.print(pkgName);
diff --git a/com/android/internal/app/procstats/ProcessStats.java b/com/android/internal/app/procstats/ProcessStats.java
index 14f5e5b5..96ba2b0c 100644
--- a/com/android/internal/app/procstats/ProcessStats.java
+++ b/com/android/internal/app/procstats/ProcessStats.java
@@ -28,6 +28,7 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DebugUtils;
import android.util.Log;
+import android.util.LongSparseArray;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
@@ -80,10 +81,10 @@ public final class ProcessStats implements Parcelable {
public static final int STATE_IMPORTANT_FOREGROUND = 2;
public static final int STATE_IMPORTANT_BACKGROUND = 3;
public static final int STATE_BACKUP = 4;
- public static final int STATE_HEAVY_WEIGHT = 5;
- public static final int STATE_SERVICE = 6;
- public static final int STATE_SERVICE_RESTARTING = 7;
- public static final int STATE_RECEIVER = 8;
+ public static final int STATE_SERVICE = 5;
+ public static final int STATE_SERVICE_RESTARTING = 6;
+ public static final int STATE_RECEIVER = 7;
+ public static final int STATE_HEAVY_WEIGHT = 8;
public static final int STATE_HOME = 9;
public static final int STATE_LAST_ACTIVITY = 10;
public static final int STATE_CACHED_ACTIVITY = 11;
@@ -140,8 +141,8 @@ public final class ProcessStats implements Parcelable {
public static final int[] NON_CACHED_PROC_STATES = new int[] {
STATE_PERSISTENT, STATE_TOP, STATE_IMPORTANT_FOREGROUND,
- STATE_IMPORTANT_BACKGROUND, STATE_BACKUP, STATE_HEAVY_WEIGHT,
- STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER
+ STATE_IMPORTANT_BACKGROUND, STATE_BACKUP,
+ STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER, STATE_HEAVY_WEIGHT
};
public static final int[] BACKGROUND_PROC_STATES = new int[] {
@@ -151,13 +152,13 @@ public final class ProcessStats implements Parcelable {
public static final int[] ALL_PROC_STATES = new int[] { STATE_PERSISTENT,
STATE_TOP, STATE_IMPORTANT_FOREGROUND, STATE_IMPORTANT_BACKGROUND, STATE_BACKUP,
- STATE_HEAVY_WEIGHT, STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER,
- STATE_HOME, STATE_LAST_ACTIVITY, STATE_CACHED_ACTIVITY,
+ STATE_SERVICE, STATE_SERVICE_RESTARTING, STATE_RECEIVER,
+ STATE_HEAVY_WEIGHT, STATE_HOME, STATE_LAST_ACTIVITY, STATE_CACHED_ACTIVITY,
STATE_CACHED_ACTIVITY_CLIENT, STATE_CACHED_EMPTY
};
// Current version of the parcel format.
- private static final int PARCEL_VERSION = 21;
+ private static final int PARCEL_VERSION = 23;
// In-memory Parcel magic number, used to detect attempts to unmarshall bad data
private static final int MAGIC = 0x50535454;
@@ -165,9 +166,8 @@ public final class ProcessStats implements Parcelable {
public String mTimePeriodStartClockStr;
public int mFlags;
- public final ProcessMap<SparseArray<PackageState>> mPackages
- = new ProcessMap<SparseArray<PackageState>>();
- public final ProcessMap<ProcessState> mProcesses = new ProcessMap<ProcessState>();
+ public final ProcessMap<LongSparseArray<PackageState>> mPackages = new ProcessMap<>();
+ public final ProcessMap<ProcessState> mProcesses = new ProcessMap<>();
public final long[] mMemFactorDurations = new long[ADJ_COUNT];
public int mMemFactor = STATE_NOTHING;
@@ -218,15 +218,16 @@ public final class ProcessStats implements Parcelable {
}
public void add(ProcessStats other) {
- ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = other.mPackages.getMap();
+ ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap =
+ other.mPackages.getMap();
for (int ip=0; ip<pkgMap.size(); ip++) {
final String pkgName = pkgMap.keyAt(ip);
- final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+ final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip);
for (int iu=0; iu<uids.size(); iu++) {
final int uid = uids.keyAt(iu);
- final SparseArray<PackageState> versions = uids.valueAt(iu);
+ final LongSparseArray<PackageState> versions = uids.valueAt(iu);
for (int iv=0; iv<versions.size(); iv++) {
- final int vers = versions.keyAt(iv);
+ final long vers = versions.keyAt(iv);
final PackageState otherState = versions.valueAt(iv);
final int NPROCS = otherState.mProcesses.size();
final int NSRVS = otherState.mServices.size();
@@ -269,7 +270,7 @@ public final class ProcessStats implements Parcelable {
ProcessState otherProc = uids.valueAt(iu);
final String name = otherProc.getName();
final String pkg = otherProc.getPackage();
- final int vers = otherProc.getVersion();
+ final long vers = otherProc.getVersion();
ProcessState thisProc = mProcesses.get(name, uid);
if (DEBUG) Slog.d(TAG, "Adding uid " + uid + " proc " + name);
if (thisProc == null) {
@@ -420,11 +421,12 @@ public final class ProcessStats implements Parcelable {
// Next reset or prune all per-package processes, and for the ones that are reset
// track this back to the common processes.
- final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+ final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap =
+ mPackages.getMap();
for (int ip=pkgMap.size()-1; ip>=0; ip--) {
- final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+ final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip);
for (int iu=uids.size()-1; iu>=0; iu--) {
- final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+ final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu);
for (int iv=vpkgs.size()-1; iv>=0; iv--) {
final PackageState pkgState = vpkgs.valueAt(iv);
for (int iproc=pkgState.mProcesses.size()-1; iproc>=0; iproc--) {
@@ -727,13 +729,14 @@ public final class ProcessStats implements Parcelable {
uids.valueAt(iu).commitStateTime(now);
}
}
- final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+ final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap =
+ mPackages.getMap();
final int NPKG = pkgMap.size();
for (int ip=0; ip<NPKG; ip++) {
- final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+ final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip);
final int NUID = uids.size();
for (int iu=0; iu<NUID; iu++) {
- final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+ final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu);
final int NVERS = vpkgs.size();
for (int iv=0; iv<NVERS; iv++) {
PackageState pkgState = vpkgs.valueAt(iv);
@@ -781,23 +784,23 @@ public final class ProcessStats implements Parcelable {
out.writeInt(uids.keyAt(iu));
final ProcessState proc = uids.valueAt(iu);
writeCommonString(out, proc.getPackage());
- out.writeInt(proc.getVersion());
+ out.writeLong(proc.getVersion());
proc.writeToParcel(out, now);
}
}
out.writeInt(NPKG);
for (int ip=0; ip<NPKG; ip++) {
writeCommonString(out, pkgMap.keyAt(ip));
- final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+ final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip);
final int NUID = uids.size();
out.writeInt(NUID);
for (int iu=0; iu<NUID; iu++) {
out.writeInt(uids.keyAt(iu));
- final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+ final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu);
final int NVERS = vpkgs.size();
out.writeInt(NVERS);
for (int iv=0; iv<NVERS; iv++) {
- out.writeInt(vpkgs.keyAt(iv));
+ out.writeLong(vpkgs.keyAt(iv));
final PackageState pkgState = vpkgs.valueAt(iv);
final int NPROCS = pkgState.mProcesses.size();
out.writeInt(NPROCS);
@@ -963,7 +966,7 @@ public final class ProcessStats implements Parcelable {
mReadError = "bad process package name";
return;
}
- final int vers = in.readInt();
+ final long vers = in.readLong();
ProcessState proc = hadData ? mProcesses.get(procName, uid) : null;
if (proc != null) {
if (!proc.readFromParcel(in, false)) {
@@ -1014,11 +1017,11 @@ public final class ProcessStats implements Parcelable {
}
while (NVERS > 0) {
NVERS--;
- final int vers = in.readInt();
+ final long vers = in.readLong();
PackageState pkgState = new PackageState(pkgName, uid);
- SparseArray<PackageState> vpkg = mPackages.get(pkgName, uid);
+ LongSparseArray<PackageState> vpkg = mPackages.get(pkgName, uid);
if (vpkg == null) {
- vpkg = new SparseArray<PackageState>();
+ vpkg = new LongSparseArray<>();
mPackages.put(pkgName, uid, vpkg);
}
vpkg.put(vers, pkgState);
@@ -1117,10 +1120,10 @@ public final class ProcessStats implements Parcelable {
if (DEBUG_PARCEL) Slog.d(TAG, "Successfully read procstats!");
}
- public PackageState getPackageStateLocked(String packageName, int uid, int vers) {
- SparseArray<PackageState> vpkg = mPackages.get(packageName, uid);
+ public PackageState getPackageStateLocked(String packageName, int uid, long vers) {
+ LongSparseArray<PackageState> vpkg = mPackages.get(packageName, uid);
if (vpkg == null) {
- vpkg = new SparseArray<PackageState>();
+ vpkg = new LongSparseArray<PackageState>();
mPackages.put(packageName, uid, vpkg);
}
PackageState as = vpkg.get(vers);
@@ -1132,7 +1135,7 @@ public final class ProcessStats implements Parcelable {
return as;
}
- public ProcessState getProcessStateLocked(String packageName, int uid, int vers,
+ public ProcessState getProcessStateLocked(String packageName, int uid, long vers,
String processName) {
final PackageState pkgState = getPackageStateLocked(packageName, uid, vers);
ProcessState ps = pkgState.mProcesses.get(processName);
@@ -1202,7 +1205,7 @@ public final class ProcessStats implements Parcelable {
return ps;
}
- public ServiceState getServiceStateLocked(String packageName, int uid, int vers,
+ public ServiceState getServiceStateLocked(String packageName, int uid, long vers,
String processName, String className) {
final ProcessStats.PackageState as = getPackageStateLocked(packageName, uid, vers);
ServiceState ss = as.mServices.get(className);
@@ -1228,16 +1231,16 @@ public final class ProcessStats implements Parcelable {
mSysMemUsage.dump(pw, " ", ALL_SCREEN_ADJ, ALL_MEM_ADJ);
sepNeeded = true;
}
- ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+ ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap = mPackages.getMap();
boolean printedHeader = false;
for (int ip=0; ip<pkgMap.size(); ip++) {
final String pkgName = pkgMap.keyAt(ip);
- final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+ final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip);
for (int iu=0; iu<uids.size(); iu++) {
final int uid = uids.keyAt(iu);
- final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+ final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu);
for (int iv=0; iv<vpkgs.size(); iv++) {
- final int vers = vpkgs.keyAt(iv);
+ final long vers = vpkgs.keyAt(iv);
final PackageState pkgState = vpkgs.valueAt(iv);
final int NPROCS = pkgState.mProcesses.size();
final int NSRVS = pkgState.mServices.size();
@@ -1531,12 +1534,13 @@ public final class ProcessStats implements Parcelable {
int[] procStates, int sortProcStates[], long now, String reqPackage,
boolean activeOnly) {
final ArraySet<ProcessState> foundProcs = new ArraySet<ProcessState>();
- final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+ final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap =
+ mPackages.getMap();
for (int ip=0; ip<pkgMap.size(); ip++) {
final String pkgName = pkgMap.keyAt(ip);
- final SparseArray<SparseArray<PackageState>> procs = pkgMap.valueAt(ip);
+ final SparseArray<LongSparseArray<PackageState>> procs = pkgMap.valueAt(ip);
for (int iu=0; iu<procs.size(); iu++) {
- final SparseArray<PackageState> vpkgs = procs.valueAt(iu);
+ final LongSparseArray<PackageState> vpkgs = procs.valueAt(iu);
final int NVERS = vpkgs.size();
for (int iv=0; iv<NVERS; iv++) {
final PackageState state = vpkgs.valueAt(iv);
@@ -1571,7 +1575,8 @@ public final class ProcessStats implements Parcelable {
public void dumpCheckinLocked(PrintWriter pw, String reqPackage) {
final long now = SystemClock.uptimeMillis();
- final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+ final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap =
+ mPackages.getMap();
pw.println("vers,5");
pw.print("period,"); pw.print(mTimePeriodStartClockStr);
pw.print(","); pw.print(mTimePeriodStartRealtime); pw.print(",");
@@ -1602,12 +1607,12 @@ public final class ProcessStats implements Parcelable {
if (reqPackage != null && !reqPackage.equals(pkgName)) {
continue;
}
- final SparseArray<SparseArray<PackageState>> uids = pkgMap.valueAt(ip);
+ final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip);
for (int iu=0; iu<uids.size(); iu++) {
final int uid = uids.keyAt(iu);
- final SparseArray<PackageState> vpkgs = uids.valueAt(iu);
+ final LongSparseArray<PackageState> vpkgs = uids.valueAt(iu);
for (int iv=0; iv<vpkgs.size(); iv++) {
- final int vers = vpkgs.keyAt(iv);
+ final long vers = vpkgs.keyAt(iv);
final PackageState pkgState = vpkgs.valueAt(iv);
final int NPROCS = pkgState.mProcesses.size();
final int NSRVS = pkgState.mServices.size();
@@ -1709,7 +1714,8 @@ public final class ProcessStats implements Parcelable {
}
public void toProto(ProtoOutputStream proto, long now) {
- final ArrayMap<String, SparseArray<SparseArray<PackageState>>> pkgMap = mPackages.getMap();
+ final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap =
+ mPackages.getMap();
proto.write(ProcessStatsSectionProto.START_REALTIME_MS, mTimePeriodStartRealtime);
proto.write(ProcessStatsSectionProto.END_REALTIME_MS,
@@ -1750,10 +1756,10 @@ public final class ProcessStats implements Parcelable {
}
final public static class ProcessStateHolder {
- public final int appVersion;
+ public final long appVersion;
public ProcessState state;
- public ProcessStateHolder(int _appVersion) {
+ public ProcessStateHolder(long _appVersion) {
appVersion = _appVersion;
}
}
diff --git a/com/android/internal/app/procstats/ServiceState.java b/com/android/internal/app/procstats/ServiceState.java
index 2e11c438..650de2ea 100644
--- a/com/android/internal/app/procstats/ServiceState.java
+++ b/com/android/internal/app/procstats/ServiceState.java
@@ -441,7 +441,7 @@ public final class ServiceState {
return totalTime;
}
- public void dumpTimesCheckin(PrintWriter pw, String pkgName, int uid, int vers,
+ public void dumpTimesCheckin(PrintWriter pw, String pkgName, int uid, long vers,
String serviceName, long now) {
dumpTimeCheckin(pw, "pkgsvc-run", pkgName, uid, vers, serviceName,
ServiceState.SERVICE_RUN, mRunCount, mRunState, mRunStartTime, now);
@@ -454,7 +454,7 @@ public final class ServiceState {
}
private void dumpTimeCheckin(PrintWriter pw, String label, String packageName,
- int uid, int vers, String serviceName, int serviceType, int opCount,
+ int uid, long vers, String serviceName, int serviceType, int opCount,
int curState, long curStartTime, long now) {
if (opCount <= 0) {
return;
diff --git a/com/android/internal/content/FileSystemProvider.java b/com/android/internal/content/FileSystemProvider.java
index d49d5723..a075705c 100644
--- a/com/android/internal/content/FileSystemProvider.java
+++ b/com/android/internal/content/FileSystemProvider.java
@@ -236,6 +236,7 @@ public abstract class FileSystemProvider extends DocumentsProvider {
moveInMediaStore(visibleFileBefore, getFileForDocId(afterDocId, true));
if (!TextUtils.equals(docId, afterDocId)) {
+ scanFile(after);
return afterDocId;
} else {
return null;
diff --git a/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java b/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java
index 931eb990..45555bf9 100644
--- a/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java
+++ b/com/android/internal/graphics/SfVsyncFrameCallbackProvider.java
@@ -26,7 +26,15 @@ import android.view.Choreographer;
*/
public final class SfVsyncFrameCallbackProvider implements AnimationFrameCallbackProvider {
- private final Choreographer mChoreographer = Choreographer.getSfInstance();
+ private final Choreographer mChoreographer;
+
+ public SfVsyncFrameCallbackProvider() {
+ mChoreographer = Choreographer.getSfInstance();
+ }
+
+ public SfVsyncFrameCallbackProvider(Choreographer choreographer) {
+ mChoreographer = choreographer;
+ }
@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
diff --git a/com/android/internal/inputmethod/InputMethodUtils.java b/com/android/internal/inputmethod/InputMethodUtils.java
index 3e231d0a..d3b4dbf1 100644
--- a/com/android/internal/inputmethod/InputMethodUtils.java
+++ b/com/android/internal/inputmethod/InputMethodUtils.java
@@ -16,6 +16,9 @@
package com.android.internal.inputmethod;
+import static android.view.inputmethod.InputMethodManager.CONTROL_WINDOW_IS_TEXT_EDITOR;
+import static android.view.inputmethod.InputMethodManager.CONTROL_WINDOW_VIEW_HAS_FOCUS;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -26,6 +29,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.res.Resources;
+import android.os.Build;
import android.os.LocaleList;
import android.os.RemoteException;
import android.provider.Settings;
@@ -33,6 +37,7 @@ import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.Log;
import android.util.Pair;
import android.util.Printer;
import android.util.Slog;
@@ -836,7 +841,6 @@ public class InputMethodUtils {
private final Resources mRes;
private final ContentResolver mResolver;
private final HashMap<String, InputMethodInfo> mMethodMap;
- private final ArrayList<InputMethodInfo> mMethodList;
/**
* On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}.
@@ -906,7 +910,6 @@ public class InputMethodUtils {
mRes = res;
mResolver = resolver;
mMethodMap = methodMap;
- mMethodList = methodList;
switchCurrentUser(userId, copyOnWrite);
}
@@ -1087,7 +1090,7 @@ public class InputMethodUtils {
final ArrayList<InputMethodInfo> res = new ArrayList<>();
for (Pair<String, ArrayList<String>> ims: imsList) {
InputMethodInfo info = mMethodMap.get(ims.first);
- if (info != null) {
+ if (info != null && !info.isVrOnly()) {
res.add(info);
}
}
@@ -1512,4 +1515,20 @@ public class InputMethodUtils {
}
return locales;
}
+
+ public static boolean isSoftInputModeStateVisibleAllowed(
+ int targetSdkVersion, int controlFlags) {
+ if (targetSdkVersion < Build.VERSION_CODES.P) {
+ // for compatibility.
+ return true;
+ }
+ if ((controlFlags & CONTROL_WINDOW_VIEW_HAS_FOCUS) == 0) {
+ return false;
+ }
+ if ((controlFlags & CONTROL_WINDOW_IS_TEXT_EDITOR) == 0) {
+ return false;
+ }
+ return true;
+ }
+
}
diff --git a/com/android/internal/location/ProviderRequest.java b/com/android/internal/location/ProviderRequest.java
index 26243e78..45fdb76f 100644
--- a/com/android/internal/location/ProviderRequest.java
+++ b/com/android/internal/location/ProviderRequest.java
@@ -33,6 +33,12 @@ public final class ProviderRequest implements Parcelable {
public long interval = Long.MAX_VALUE;
/**
+ * Whether provider shall make stronger than normal tradeoffs to substantially restrict power
+ * use.
+ */
+ public boolean lowPowerMode = false;
+
+ /**
* A more detailed set of requests.
* <p>Location Providers can optionally use this to
* fine tune location updates, for example when there
@@ -41,26 +47,29 @@ public final class ProviderRequest implements Parcelable {
*/
public List<LocationRequest> locationRequests = new ArrayList<LocationRequest>();
- public ProviderRequest() { }
+ public ProviderRequest() {
+ }
public static final Parcelable.Creator<ProviderRequest> CREATOR =
new Parcelable.Creator<ProviderRequest>() {
- @Override
- public ProviderRequest createFromParcel(Parcel in) {
- ProviderRequest request = new ProviderRequest();
- request.reportLocation = in.readInt() == 1;
- request.interval = in.readLong();
- int count = in.readInt();
- for (int i = 0; i < count; i++) {
- request.locationRequests.add(LocationRequest.CREATOR.createFromParcel(in));
- }
- return request;
- }
- @Override
- public ProviderRequest[] newArray(int size) {
- return new ProviderRequest[size];
- }
- };
+ @Override
+ public ProviderRequest createFromParcel(Parcel in) {
+ ProviderRequest request = new ProviderRequest();
+ request.reportLocation = in.readInt() == 1;
+ request.interval = in.readLong();
+ request.lowPowerMode = in.readBoolean();
+ int count = in.readInt();
+ for (int i = 0; i < count; i++) {
+ request.locationRequests.add(LocationRequest.CREATOR.createFromParcel(in));
+ }
+ return request;
+ }
+
+ @Override
+ public ProviderRequest[] newArray(int size) {
+ return new ProviderRequest[size];
+ }
+ };
@Override
public int describeContents() {
@@ -71,6 +80,7 @@ public final class ProviderRequest implements Parcelable {
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeInt(reportLocation ? 1 : 0);
parcel.writeLong(interval);
+ parcel.writeBoolean(lowPowerMode);
parcel.writeInt(locationRequests.size());
for (LocationRequest request : locationRequests) {
request.writeToParcel(parcel, flags);
@@ -85,6 +95,7 @@ public final class ProviderRequest implements Parcelable {
s.append("ON");
s.append(" interval=");
TimeUtils.formatDuration(interval, s);
+ s.append(" lowPowerMode=" + lowPowerMode);
} else {
s.append("OFF");
}
diff --git a/com/android/internal/os/BackgroundThread.java b/com/android/internal/os/BackgroundThread.java
index cffba017..7558f8ce 100644
--- a/com/android/internal/os/BackgroundThread.java
+++ b/com/android/internal/os/BackgroundThread.java
@@ -35,7 +35,7 @@ public final class BackgroundThread extends HandlerThread {
if (sInstance == null) {
sInstance = new BackgroundThread();
sInstance.start();
- sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER);
sHandler = new Handler(sInstance.getLooper());
}
}
diff --git a/com/android/internal/os/BatteryStatsImpl.java b/com/android/internal/os/BatteryStatsImpl.java
index 56d0bb22..72f07b7c 100644
--- a/com/android/internal/os/BatteryStatsImpl.java
+++ b/com/android/internal/os/BatteryStatsImpl.java
@@ -30,6 +30,7 @@ import android.net.wifi.WifiManager;
import android.os.BatteryManager;
import android.os.BatteryStats;
import android.os.Build;
+import android.os.connectivity.CellularBatteryStats;
import android.os.FileUtils;
import android.os.Handler;
import android.os.IBatteryPropertiesRegistrar;
@@ -44,6 +45,7 @@ import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.WorkSource;
+import android.os.WorkSource.WorkChain;
import android.telephony.DataConnectionRealTimeInfo;
import android.telephony.ModemActivityInfo;
import android.telephony.ServiceState;
@@ -51,6 +53,7 @@ import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.IntArray;
import android.util.Log;
import android.util.LogWriter;
import android.util.LongSparseArray;
@@ -77,6 +80,7 @@ import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.JournaledFile;
import com.android.internal.util.XmlUtils;
+import java.util.List;
import libcore.util.EmptyArray;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -120,7 +124,7 @@ public class BatteryStatsImpl extends BatteryStats {
private static final int MAGIC = 0xBA757475; // 'BATSTATS'
// Current on-disk Parcel version
- private static final int VERSION = 169 + (USE_OLD_HISTORY ? 1000 : 0);
+ private static final int VERSION = 172 + (USE_OLD_HISTORY ? 1000 : 0);
// Maximum number of items we will record in the history.
private static final int MAX_HISTORY_ITEMS;
@@ -188,6 +192,8 @@ public class BatteryStatsImpl extends BatteryStats {
@VisibleForTesting
protected KernelUidCpuFreqTimeReader mKernelUidCpuFreqTimeReader =
new KernelUidCpuFreqTimeReader();
+ @VisibleForTesting
+ protected KernelSingleUidTimeReader mKernelSingleUidTimeReader;
private final KernelMemoryBandwidthStats mKernelMemoryBandwidthStats
= new KernelMemoryBandwidthStats();
@@ -196,6 +202,18 @@ public class BatteryStatsImpl extends BatteryStats {
return mKernelMemoryStats;
}
+ @GuardedBy("this")
+ public boolean mPerProcStateCpuTimesAvailable = true;
+
+ /**
+ * Uids for which per-procstate cpu times need to be updated.
+ *
+ * Contains uid -> procState mappings.
+ */
+ @GuardedBy("this")
+ @VisibleForTesting
+ protected final SparseIntArray mPendingUids = new SparseIntArray();
+
/** Container for Resource Power Manager stats. Updated by updateRpmStatsLocked. */
private final RpmStats mTmpRpmStats = new RpmStats();
/** The soonest the RPM stats can be updated after it was last updated. */
@@ -268,6 +286,138 @@ public class BatteryStatsImpl extends BatteryStats {
}
}
+ /**
+ * Update per-freq cpu times for all the uids in {@link #mPendingUids}.
+ */
+ public void updateProcStateCpuTimes() {
+ final SparseIntArray uidStates;
+ synchronized (BatteryStatsImpl.this) {
+ if(!initKernelSingleUidTimeReaderLocked()) {
+ return;
+ }
+
+ if (mPendingUids.size() == 0) {
+ return;
+ }
+ uidStates = mPendingUids.clone();
+ mPendingUids.clear();
+ }
+ for (int i = uidStates.size() - 1; i >= 0; --i) {
+ final int uid = uidStates.keyAt(i);
+ final int procState = uidStates.valueAt(i);
+ final int[] isolatedUids;
+ final Uid u;
+ final boolean onBattery;
+ synchronized (BatteryStatsImpl.this) {
+ // It's possible that uid no longer exists and any internal references have
+ // already been deleted, so using {@link #getAvailableUidStatsLocked} to avoid
+ // creating an UidStats object if it doesn't already exist.
+ u = getAvailableUidStatsLocked(uid);
+ if (u == null) {
+ continue;
+ }
+ if (u.mChildUids == null) {
+ isolatedUids = null;
+ } else {
+ isolatedUids = u.mChildUids.toArray();
+ for (int j = isolatedUids.length - 1; j >= 0; --j) {
+ isolatedUids[j] = u.mChildUids.get(j);
+ }
+ }
+ onBattery = mOnBatteryInternal;
+ }
+ long[] cpuTimesMs = mKernelSingleUidTimeReader.readDeltaMs(uid);
+ if (isolatedUids != null) {
+ for (int j = isolatedUids.length - 1; j >= 0; --j) {
+ cpuTimesMs = addCpuTimes(cpuTimesMs,
+ mKernelSingleUidTimeReader.readDeltaMs(isolatedUids[j]));
+ }
+ }
+ if (onBattery && cpuTimesMs != null) {
+ synchronized (BatteryStatsImpl.this) {
+ u.addProcStateTimesMs(procState, cpuTimesMs);
+ u.addProcStateScreenOffTimesMs(procState, cpuTimesMs);
+ }
+ }
+ }
+ }
+
+ /**
+ * When the battery/screen state changes, we don't attribute the cpu times to any process
+ * but we still need to snapshots of all uids to get correct deltas later on. Since we
+ * already read this data for updating per-freq cpu times, we can use the same data for
+ * per-procstate cpu times.
+ */
+ public void copyFromAllUidsCpuTimes() {
+ synchronized (BatteryStatsImpl.this) {
+ if(!initKernelSingleUidTimeReaderLocked()) {
+ return;
+ }
+
+ final SparseArray<long[]> allUidCpuFreqTimesMs =
+ mKernelUidCpuFreqTimeReader.getAllUidCpuFreqTimeMs();
+ for (int i = allUidCpuFreqTimesMs.size() - 1; i >= 0; --i) {
+ final int uid = allUidCpuFreqTimesMs.keyAt(i);
+ final Uid u = getAvailableUidStatsLocked(mapUid(uid));
+ if (u == null) {
+ continue;
+ }
+ final long[] cpuTimesMs = allUidCpuFreqTimesMs.valueAt(i);
+ if (cpuTimesMs == null) {
+ continue;
+ }
+ final long[] deltaTimesMs = mKernelSingleUidTimeReader.computeDelta(
+ uid, cpuTimesMs.clone());
+ if (mOnBatteryInternal && deltaTimesMs != null) {
+ final int procState;
+ final int idx = mPendingUids.indexOfKey(uid);
+ if (idx >= 0) {
+ procState = mPendingUids.valueAt(idx);
+ mPendingUids.removeAt(idx);
+ } else {
+ procState = u.mProcessState;
+ }
+ if (procState >= 0 && procState < Uid.NUM_PROCESS_STATE) {
+ u.addProcStateTimesMs(procState, deltaTimesMs);
+ u.addProcStateScreenOffTimesMs(procState, deltaTimesMs);
+ }
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public long[] addCpuTimes(long[] timesA, long[] timesB) {
+ if (timesA != null && timesB != null) {
+ for (int i = timesA.length - 1; i >= 0; --i) {
+ timesA[i] += timesB[i];
+ }
+ return timesA;
+ }
+ return timesA == null ? (timesB == null ? null : timesB) : timesA;
+ }
+
+ @GuardedBy("this")
+ private boolean initKernelSingleUidTimeReaderLocked() {
+ if (mKernelSingleUidTimeReader == null) {
+ if (mPowerProfile == null) {
+ return false;
+ }
+ if (mCpuFreqs == null) {
+ mCpuFreqs = mKernelUidCpuFreqTimeReader.readFreqs(mPowerProfile);
+ }
+ if (mCpuFreqs != null) {
+ mKernelSingleUidTimeReader = new KernelSingleUidTimeReader(mCpuFreqs.length);
+ } else {
+ mPerProcStateCpuTimesAvailable = mKernelUidCpuFreqTimeReader.allUidTimesAvailable();
+ return false;
+ }
+ }
+ mPerProcStateCpuTimesAvailable = mKernelUidCpuFreqTimeReader.allUidTimesAvailable()
+ && mKernelSingleUidTimeReader.singleUidCpuTimesAvailable();
+ return true;
+ }
+
public interface Clocks {
public long elapsedRealtime();
public long uptimeMillis();
@@ -293,9 +443,11 @@ public class BatteryStatsImpl extends BatteryStats {
Future<?> scheduleSync(String reason, int flags);
Future<?> scheduleCpuSyncDueToRemovedUid(int uid);
+ Future<?> scheduleReadProcStateCpuTimes();
+ Future<?> scheduleCopyFromAllUidsCpuTimes();
}
- public final MyHandler mHandler;
+ public Handler mHandler;
private ExternalStatsSync mExternalSync = null;
@VisibleForTesting
protected UserInfoProvider mUserInfoProvider = null;
@@ -3623,6 +3775,7 @@ public class BatteryStatsImpl extends BatteryStats {
+ " and battery is " + (unplugged ? "on" : "off"));
}
updateCpuTimeLocked();
+ mExternalSync.scheduleCopyFromAllUidsCpuTimes();
mOnBatteryTimeBase.setRunning(unplugged, uptime, realtime);
if (updateOnBatteryTimeBase) {
@@ -3652,6 +3805,8 @@ public class BatteryStatsImpl extends BatteryStats {
public void addIsolatedUidLocked(int isolatedUid, int appUid) {
mIsolatedUids.put(isolatedUid, appUid);
StatsLog.write(StatsLog.ISOLATED_UID_CHANGED, appUid, isolatedUid, 1);
+ final Uid u = getUidStatsLocked(appUid);
+ u.addIsolatedUid(isolatedUid);
}
/**
@@ -3672,11 +3827,17 @@ public class BatteryStatsImpl extends BatteryStats {
* @see #scheduleRemoveIsolatedUidLocked(int, int)
*/
public void removeIsolatedUidLocked(int isolatedUid) {
- StatsLog.write(
- StatsLog.ISOLATED_UID_CHANGED, mIsolatedUids.get(isolatedUid, -1), isolatedUid, 0);
- mIsolatedUids.delete(isolatedUid);
- mKernelUidCpuTimeReader.removeUid(isolatedUid);
- mKernelUidCpuFreqTimeReader.removeUid(isolatedUid);
+ StatsLog.write(
+ StatsLog.ISOLATED_UID_CHANGED, mIsolatedUids.get(isolatedUid, -1), isolatedUid, 0);
+ final int idx = mIsolatedUids.indexOfKey(isolatedUid);
+ if (idx >= 0) {
+ final int ownerUid = mIsolatedUids.valueAt(idx);
+ final Uid u = getUidStatsLocked(ownerUid);
+ u.removeIsolatedUid(isolatedUid);
+ mIsolatedUids.removeAt(idx);
+ }
+ mKernelUidCpuTimeReader.removeUid(isolatedUid);
+ mKernelUidCpuFreqTimeReader.removeUid(isolatedUid);
}
public int mapUid(int uid) {
@@ -3905,8 +4066,8 @@ public class BatteryStatsImpl extends BatteryStats {
private String mInitialAcquireWakeName;
private int mInitialAcquireWakeUid = -1;
- public void noteStartWakeLocked(int uid, int pid, String name, String historyName, int type,
- boolean unimportantForLogging, long elapsedRealtime, long uptime) {
+ public void noteStartWakeLocked(int uid, int pid, WorkChain wc, String name, String historyName,
+ int type, boolean unimportantForLogging, long elapsedRealtime, long uptime) {
uid = mapUid(uid);
if (type == WAKE_TYPE_PARTIAL) {
// Only care about partial wake locks, since full wake locks
@@ -3954,12 +4115,18 @@ public class BatteryStatsImpl extends BatteryStats {
}
requestWakelockCpuUpdate();
}
+
getUidStatsLocked(uid).noteStartWakeLocked(pid, name, type, elapsedRealtime);
+
+ if (wc != null) {
+ StatsLog.write(
+ StatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(), wc.getTags(), type, name, 1);
+ }
}
}
- public void noteStopWakeLocked(int uid, int pid, String name, String historyName, int type,
- long elapsedRealtime, long uptime) {
+ public void noteStopWakeLocked(int uid, int pid, WorkChain wc, String name, String historyName,
+ int type, long elapsedRealtime, long uptime) {
uid = mapUid(uid);
if (type == WAKE_TYPE_PARTIAL) {
mWakeLockNesting--;
@@ -3989,7 +4156,12 @@ public class BatteryStatsImpl extends BatteryStats {
}
requestWakelockCpuUpdate();
}
+
getUidStatsLocked(uid).noteStopWakeLocked(pid, name, type, elapsedRealtime);
+ if (wc != null) {
+ StatsLog.write(
+ StatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(), wc.getTags(), type, name, 0);
+ }
}
}
@@ -3999,8 +4171,17 @@ public class BatteryStatsImpl extends BatteryStats {
final long uptime = mClocks.uptimeMillis();
final int N = ws.size();
for (int i=0; i<N; i++) {
- noteStartWakeLocked(ws.get(i), pid, name, historyName, type, unimportantForLogging,
- elapsedRealtime, uptime);
+ noteStartWakeLocked(ws.get(i), pid, null, name, historyName, type,
+ unimportantForLogging, elapsedRealtime, uptime);
+ }
+
+ List<WorkChain> wcs = ws.getWorkChains();
+ if (wcs != null) {
+ for (int i = 0; i < wcs.size(); ++i) {
+ final WorkChain wc = wcs.get(i);
+ noteStartWakeLocked(wc.getAttributionUid(), pid, wc, name, historyName, type,
+ unimportantForLogging, elapsedRealtime, uptime);
+ }
}
}
@@ -4009,17 +4190,46 @@ public class BatteryStatsImpl extends BatteryStats {
String newHistoryName, int newType, boolean newUnimportantForLogging) {
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
+
+ List<WorkChain>[] wcs = WorkSource.diffChains(ws, newWs);
+
// For correct semantics, we start the need worksources first, so that we won't
// make inappropriate history items as if all wake locks went away and new ones
// appeared. This is okay because tracking of wake locks allows nesting.
+ //
+ // First the starts :
final int NN = newWs.size();
for (int i=0; i<NN; i++) {
- noteStartWakeLocked(newWs.get(i), newPid, newName, newHistoryName, newType,
+ noteStartWakeLocked(newWs.get(i), newPid, null, newName, newHistoryName, newType,
newUnimportantForLogging, elapsedRealtime, uptime);
}
+ if (wcs != null) {
+ List<WorkChain> newChains = wcs[0];
+ if (newChains != null) {
+ for (int i = 0; i < newChains.size(); ++i) {
+ final WorkChain newChain = newChains.get(i);
+ noteStartWakeLocked(newChain.getAttributionUid(), newPid, newChain, newName,
+ newHistoryName, newType, newUnimportantForLogging, elapsedRealtime,
+ uptime);
+ }
+ }
+ }
+
+ // Then the stops :
final int NO = ws.size();
for (int i=0; i<NO; i++) {
- noteStopWakeLocked(ws.get(i), pid, name, historyName, type, elapsedRealtime, uptime);
+ noteStopWakeLocked(ws.get(i), pid, null, name, historyName, type, elapsedRealtime,
+ uptime);
+ }
+ if (wcs != null) {
+ List<WorkChain> goneChains = wcs[1];
+ if (goneChains != null) {
+ for (int i = 0; i < goneChains.size(); ++i) {
+ final WorkChain goneChain = goneChains.get(i);
+ noteStopWakeLocked(goneChain.getAttributionUid(), pid, goneChain, name,
+ historyName, type, elapsedRealtime, uptime);
+ }
+ }
}
}
@@ -4029,7 +4239,17 @@ public class BatteryStatsImpl extends BatteryStats {
final long uptime = mClocks.uptimeMillis();
final int N = ws.size();
for (int i=0; i<N; i++) {
- noteStopWakeLocked(ws.get(i), pid, name, historyName, type, elapsedRealtime, uptime);
+ noteStopWakeLocked(ws.get(i), pid, null, name, historyName, type, elapsedRealtime,
+ uptime);
+ }
+
+ List<WorkChain> wcs = ws.getWorkChains();
+ if (wcs != null) {
+ for (int i = 0; i < wcs.size(); ++i) {
+ final WorkChain wc = wcs.get(i);
+ noteStopWakeLocked(wc.getAttributionUid(), pid, wc, name, historyName, type,
+ elapsedRealtime, uptime);
+ }
}
}
@@ -4273,10 +4493,10 @@ public class BatteryStatsImpl extends BatteryStats {
updateTimeBasesLocked(mOnBatteryTimeBase.isRunning(), state,
mClocks.uptimeMillis() * 1000, elapsedRealtime * 1000);
// Fake a wake lock, so we consider the device waked as long as the screen is on.
- noteStartWakeLocked(-1, -1, "screen", null, WAKE_TYPE_PARTIAL, false,
+ noteStartWakeLocked(-1, -1, null, "screen", null, WAKE_TYPE_PARTIAL, false,
elapsedRealtime, uptime);
} else if (isScreenOn(oldState)) {
- noteStopWakeLocked(-1, -1, "screen", "screen", WAKE_TYPE_PARTIAL,
+ noteStopWakeLocked(-1, -1, null, "screen", "screen", WAKE_TYPE_PARTIAL,
elapsedRealtime, uptime);
updateTimeBasesLocked(mOnBatteryTimeBase.isRunning(), state,
mClocks.uptimeMillis() * 1000, elapsedRealtime * 1000);
@@ -4389,6 +4609,7 @@ public class BatteryStatsImpl extends BatteryStats {
+ Integer.toHexString(mHistoryCur.states));
addHistoryRecordLocked(elapsedRealtime, uptime);
mMobileRadioPowerState = powerState;
+ StatsLog.write(StatsLog.MOBILE_RADIO_POWER_STATE_CHANGED, uid, powerState);
if (active) {
mMobileRadioActiveTimer.startRunningLocked(elapsedRealtime);
mMobileRadioActivePerAppTimer.startRunningLocked(elapsedRealtime);
@@ -4426,7 +4647,7 @@ public class BatteryStatsImpl extends BatteryStats {
}
}
- public void noteDeviceIdleModeLocked(int mode, String activeReason, int activeUid) {
+ public void noteDeviceIdleModeLocked(final int mode, String activeReason, int activeUid) {
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
boolean nowIdling = mode == DEVICE_IDLE_MODE_DEEP;
@@ -4445,6 +4666,13 @@ public class BatteryStatsImpl extends BatteryStats {
addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_ACTIVE,
activeReason, activeUid);
}
+ if (mDeviceIdling != nowIdling || mDeviceLightIdling != nowLightIdling) {
+ int statsmode;
+ if (nowIdling) statsmode = DEVICE_IDLE_MODE_DEEP;
+ else if (nowLightIdling) statsmode = DEVICE_IDLE_MODE_LIGHT;
+ else statsmode = DEVICE_IDLE_MODE_OFF;
+ StatsLog.write(StatsLog.DEVICE_IDLING_MODE_STATE_CHANGED, statsmode);
+ }
if (mDeviceIdling != nowIdling) {
mDeviceIdling = nowIdling;
int stepState = nowIdling ? STEP_LEVEL_MODE_DEVICE_IDLE : 0;
@@ -4489,14 +4717,16 @@ public class BatteryStatsImpl extends BatteryStats {
mDeviceIdleModeFullTimer.startRunningLocked(elapsedRealtime);
}
mDeviceIdleMode = mode;
+ StatsLog.write(StatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mode);
}
}
- public void notePackageInstalledLocked(String pkgName, int versionCode) {
+ public void notePackageInstalledLocked(String pkgName, long versionCode) {
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
+ // XXX need to figure out what to do with long version codes.
addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_PACKAGE_INSTALLED,
- pkgName, versionCode);
+ pkgName, (int)versionCode);
PackageChange pc = new PackageChange();
pc.mPackageName = pkgName;
pc.mUpdate = true;
@@ -5085,6 +5315,7 @@ public class BatteryStatsImpl extends BatteryStats {
+ Integer.toHexString(mHistoryCur.states));
addHistoryRecordLocked(elapsedRealtime, uptime);
mWifiRadioPowerState = powerState;
+ StatsLog.write(StatsLog.WIFI_RADIO_POWER_STATE_CHANGED, uid, powerState);
}
}
@@ -5888,6 +6119,11 @@ public class BatteryStatsImpl extends BatteryStats {
LongSamplingCounterArray mCpuFreqTimeMs;
LongSamplingCounterArray mScreenOffCpuFreqTimeMs;
+ LongSamplingCounterArray[] mProcStateTimeMs;
+ LongSamplingCounterArray[] mProcStateScreenOffTimeMs;
+
+ IntArray mChildUids;
+
/**
* The statistics we have collected for this uid's wake locks.
*/
@@ -5973,42 +6209,109 @@ public class BatteryStatsImpl extends BatteryStats {
mProcessStateTimer = new StopwatchTimer[NUM_PROCESS_STATE];
}
+ @VisibleForTesting
+ public void setProcessStateForTest(int procState) {
+ mProcessState = procState;
+ }
+
@Override
public long[] getCpuFreqTimes(int which) {
- if (mCpuFreqTimeMs == null) {
+ return nullIfAllZeros(mCpuFreqTimeMs, which);
+ }
+
+ @Override
+ public long[] getScreenOffCpuFreqTimes(int which) {
+ return nullIfAllZeros(mScreenOffCpuFreqTimeMs, which);
+ }
+
+ @Override
+ public long[] getCpuFreqTimes(int which, int procState) {
+ if (which < 0 || which >= NUM_PROCESS_STATE) {
return null;
}
- final long[] cpuFreqTimes = mCpuFreqTimeMs.getCountsLocked(which);
- if (cpuFreqTimes == null) {
+ if (mProcStateTimeMs == null) {
return null;
}
- // Return cpuFreqTimes only if atleast one of the elements in non-zero.
- for (int i = 0; i < cpuFreqTimes.length; ++i) {
- if (cpuFreqTimes[i] != 0) {
- return cpuFreqTimes;
- }
+ if (!mBsi.mPerProcStateCpuTimesAvailable) {
+ mProcStateTimeMs = null;
+ return null;
}
- return null;
+ return nullIfAllZeros(mProcStateTimeMs[procState], which);
}
@Override
- public long[] getScreenOffCpuFreqTimes(int which) {
- if (mScreenOffCpuFreqTimeMs == null) {
+ public long[] getScreenOffCpuFreqTimes(int which, int procState) {
+ if (which < 0 || which >= NUM_PROCESS_STATE) {
+ return null;
+ }
+ if (mProcStateScreenOffTimeMs == null) {
+ return null;
+ }
+ if (!mBsi.mPerProcStateCpuTimesAvailable) {
+ mProcStateScreenOffTimeMs = null;
+ return null;
+ }
+ return nullIfAllZeros(mProcStateScreenOffTimeMs[procState], which);
+ }
+
+ public void addIsolatedUid(int isolatedUid) {
+ if (mChildUids == null) {
+ mChildUids = new IntArray();
+ } else if (mChildUids.indexOf(isolatedUid) >= 0) {
+ return;
+ }
+ mChildUids.add(isolatedUid);
+ }
+
+ public void removeIsolatedUid(int isolatedUid) {
+ final int idx = mChildUids == null ? -1 : mChildUids.indexOf(isolatedUid);
+ if (idx < 0) {
+ return;
+ }
+ mChildUids.remove(idx);
+ }
+
+ private long[] nullIfAllZeros(LongSamplingCounterArray cpuTimesMs, int which) {
+ if (cpuTimesMs == null) {
return null;
}
- final long[] cpuFreqTimes = mScreenOffCpuFreqTimeMs.getCountsLocked(which);
- if (cpuFreqTimes == null) {
+ final long[] counts = cpuTimesMs.getCountsLocked(which);
+ if (counts == null) {
return null;
}
- // Return cpuFreqTimes only if atleast one of the elements in non-zero.
- for (int i = 0; i < cpuFreqTimes.length; ++i) {
- if (cpuFreqTimes[i] != 0) {
- return cpuFreqTimes;
+ // Return counts only if at least one of the elements is non-zero.
+ for (int i = counts.length - 1; i >= 0; --i) {
+ if (counts[i] != 0) {
+ return counts;
}
}
return null;
}
+ private void addProcStateTimesMs(int procState, long[] cpuTimesMs) {
+ if (mProcStateTimeMs == null) {
+ mProcStateTimeMs = new LongSamplingCounterArray[NUM_PROCESS_STATE];
+ }
+ if (mProcStateTimeMs[procState] == null
+ || mProcStateTimeMs[procState].getSize() != cpuTimesMs.length) {
+ mProcStateTimeMs[procState] = new LongSamplingCounterArray(
+ mBsi.mOnBatteryTimeBase);
+ }
+ mProcStateTimeMs[procState].addCountLocked(cpuTimesMs);
+ }
+
+ private void addProcStateScreenOffTimesMs(int procState, long[] cpuTimesMs) {
+ if (mProcStateScreenOffTimeMs == null) {
+ mProcStateScreenOffTimeMs = new LongSamplingCounterArray[NUM_PROCESS_STATE];
+ }
+ if (mProcStateScreenOffTimeMs[procState] == null
+ || mProcStateScreenOffTimeMs[procState].getSize() != cpuTimesMs.length) {
+ mProcStateScreenOffTimeMs[procState] = new LongSamplingCounterArray(
+ mBsi.mOnBatteryScreenOffTimeBase);
+ }
+ mProcStateScreenOffTimeMs[procState].addCountLocked(cpuTimesMs);
+ }
+
@Override
public Timer getAggregatedPartialWakelockTimer() {
return mAggregatedPartialWakelockTimer;
@@ -6988,6 +7291,21 @@ public class BatteryStatsImpl extends BatteryStats {
mScreenOffCpuFreqTimeMs.reset(false);
}
+ if (mProcStateTimeMs != null) {
+ for (LongSamplingCounterArray counters : mProcStateTimeMs) {
+ if (counters != null) {
+ counters.reset(false);
+ }
+ }
+ }
+ if (mProcStateScreenOffTimeMs != null) {
+ for (LongSamplingCounterArray counters : mProcStateScreenOffTimeMs) {
+ if (counters != null) {
+ counters.reset(false);
+ }
+ }
+ }
+
resetLongCounterIfNotNull(mMobileRadioApWakeupCount, false);
resetLongCounterIfNotNull(mWifiRadioApWakeupCount, false);
@@ -7178,6 +7496,20 @@ public class BatteryStatsImpl extends BatteryStats {
mScreenOffCpuFreqTimeMs.detach();
}
+ if (mProcStateTimeMs != null) {
+ for (LongSamplingCounterArray counters : mProcStateTimeMs) {
+ if (counters != null) {
+ counters.detach();
+ }
+ }
+ }
+ if (mProcStateScreenOffTimeMs != null) {
+ for (LongSamplingCounterArray counters : mProcStateScreenOffTimeMs) {
+ if (counters != null) {
+ counters.detach();
+ }
+ }
+ }
detachLongCounterIfNotNull(mMobileRadioApWakeupCount);
detachLongCounterIfNotNull(mWifiRadioApWakeupCount);
}
@@ -7438,6 +7770,22 @@ public class BatteryStatsImpl extends BatteryStats {
LongSamplingCounterArray.writeToParcel(out, mCpuFreqTimeMs);
LongSamplingCounterArray.writeToParcel(out, mScreenOffCpuFreqTimeMs);
+ if (mProcStateTimeMs != null) {
+ out.writeInt(mProcStateTimeMs.length);
+ for (LongSamplingCounterArray counters : mProcStateTimeMs) {
+ LongSamplingCounterArray.writeToParcel(out, counters);
+ }
+ } else {
+ out.writeInt(0);
+ }
+ if (mProcStateScreenOffTimeMs != null) {
+ out.writeInt(mProcStateScreenOffTimeMs.length);
+ for (LongSamplingCounterArray counters : mProcStateScreenOffTimeMs) {
+ LongSamplingCounterArray.writeToParcel(out, counters);
+ }
+ } else {
+ out.writeInt(0);
+ }
if (mMobileRadioApWakeupCount != null) {
out.writeInt(1);
@@ -7739,6 +8087,27 @@ public class BatteryStatsImpl extends BatteryStats {
mScreenOffCpuFreqTimeMs = LongSamplingCounterArray.readFromParcel(
in, mBsi.mOnBatteryScreenOffTimeBase);
+ int length = in.readInt();
+ if (length == NUM_PROCESS_STATE) {
+ mProcStateTimeMs = new LongSamplingCounterArray[length];
+ for (int procState = 0; procState < length; ++procState) {
+ mProcStateTimeMs[procState] = LongSamplingCounterArray.readFromParcel(
+ in, mBsi.mOnBatteryTimeBase);
+ }
+ } else {
+ mProcStateTimeMs = null;
+ }
+ length = in.readInt();
+ if (length == NUM_PROCESS_STATE) {
+ mProcStateScreenOffTimeMs = new LongSamplingCounterArray[length];
+ for (int procState = 0; procState < length; ++procState) {
+ mProcStateScreenOffTimeMs[procState] = LongSamplingCounterArray.readFromParcel(
+ in, mBsi.mOnBatteryScreenOffTimeBase);
+ }
+ } else {
+ mProcStateScreenOffTimeMs = null;
+ }
+
if (in.readInt() != 0) {
mMobileRadioApWakeupCount = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
} else {
@@ -8660,23 +9029,7 @@ public class BatteryStatsImpl extends BatteryStats {
// Make special note of Foreground Services
final boolean userAwareService =
(procState == ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
- if (procState == ActivityManager.PROCESS_STATE_NONEXISTENT) {
- uidRunningState = ActivityManager.PROCESS_STATE_NONEXISTENT;
- } else if (procState == ActivityManager.PROCESS_STATE_TOP) {
- uidRunningState = PROCESS_STATE_TOP;
- } else if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
- // Persistent and other foreground states go here.
- uidRunningState = PROCESS_STATE_FOREGROUND_SERVICE;
- } else if (procState <= ActivityManager.PROCESS_STATE_TOP_SLEEPING) {
- uidRunningState = PROCESS_STATE_TOP_SLEEPING;
- } else if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
- // Persistent and other foreground states go here.
- uidRunningState = PROCESS_STATE_FOREGROUND;
- } else if (procState <= ActivityManager.PROCESS_STATE_RECEIVER) {
- uidRunningState = PROCESS_STATE_BACKGROUND;
- } else {
- uidRunningState = PROCESS_STATE_CACHED;
- }
+ uidRunningState = BatteryStats.mapToInternalProcessState(procState);
if (mProcessState == uidRunningState && userAwareService == mInForegroundService) {
return;
@@ -8688,6 +9041,18 @@ public class BatteryStatsImpl extends BatteryStats {
if (mProcessState != ActivityManager.PROCESS_STATE_NONEXISTENT) {
mProcessStateTimer[mProcessState].stopRunningLocked(elapsedRealtimeMs);
+
+ if (mBsi.mPerProcStateCpuTimesAvailable) {
+ if (mBsi.mPendingUids.size() == 0) {
+ mBsi.mExternalSync.scheduleReadProcStateCpuTimes();
+ }
+ if (mBsi.mPendingUids.indexOfKey(mUid) < 0
+ || ArrayUtils.contains(CRITICAL_PROC_STATES, mProcessState)) {
+ mBsi.mPendingUids.put(mUid, mProcessState);
+ }
+ } else {
+ mBsi.mPendingUids.clear();
+ }
}
mProcessState = uidRunningState;
if (uidRunningState != ActivityManager.PROCESS_STATE_NONEXISTENT) {
@@ -8923,13 +9288,9 @@ public class BatteryStatsImpl extends BatteryStats {
Wakelock wl = mWakelockStats.startObject(name);
if (wl != null) {
getWakelockTimerLocked(wl, type).startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Hopefully use a worksource instead of a uid (so move elsewhere)
- StatsLog.write(StatsLog.WAKELOCK_STATE_CHANGED, getUid(), type, name, 1);
}
if (type == WAKE_TYPE_PARTIAL) {
createAggregatedPartialWakelockTimerLocked().startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.UID_WAKELOCK_STATE_CHANGED, getUid(), type, 1);
if (pid >= 0) {
Pid p = getPidStatsLocked(pid);
if (p.mWakeNesting++ == 0) {
@@ -8945,18 +9306,11 @@ public class BatteryStatsImpl extends BatteryStats {
StopwatchTimer wlt = getWakelockTimerLocked(wl, type);
wlt.stopRunningLocked(elapsedRealtimeMs);
if (!wlt.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.WAKELOCK_STATE_CHANGED, getUid(), type, name, 0);
}
}
if (type == WAKE_TYPE_PARTIAL) {
if (mAggregatedPartialWakelockTimer != null) {
mAggregatedPartialWakelockTimer.stopRunningLocked(elapsedRealtimeMs);
- if (!mAggregatedPartialWakelockTimer.isRunningLocked()) {
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.UID_WAKELOCK_STATE_CHANGED, getUid(), type,
- 0);
- }
}
if (pid >= 0) {
Pid p = mPids.get(pid);
@@ -9280,7 +9634,7 @@ public class BatteryStatsImpl extends BatteryStats {
if (pc.mUpdate) {
out.startTag(null, "upd");
out.attribute(null, "pkg", pc.mPackageName);
- out.attribute(null, "ver", Integer.toString(pc.mVersionCode));
+ out.attribute(null, "ver", Long.toString(pc.mVersionCode));
out.endTag(null, "upd");
} else {
out.startTag(null, "rem");
@@ -9409,7 +9763,7 @@ public class BatteryStatsImpl extends BatteryStats {
pc.mUpdate = true;
pc.mPackageName = parser.getAttributeValue(null, "pkg");
String verStr = parser.getAttributeValue(null, "ver");
- pc.mVersionCode = verStr != null ? Integer.parseInt(verStr) : 0;
+ pc.mVersionCode = verStr != null ? Long.parseLong(verStr) : 0;
dit.mPackageChanges.add(pc);
XmlUtils.skipCurrentTag(parser);
} else if (tagName.equals("rem")) {
@@ -10934,6 +11288,7 @@ public class BatteryStatsImpl extends BatteryStats {
final int numWakelocks = partialTimers == null ? 0 : partialTimers.size();
final int numClusters = mPowerProfile.getNumCpuClusters();
mWakeLockAllocationsUs = null;
+ final long startTimeMs = mClocks.uptimeMillis();
mKernelUidCpuFreqTimeReader.readDelta((uid, cpuFreqTimeMs) -> {
uid = mapUid(uid);
if (Process.isIsolated(uid)) {
@@ -10999,6 +11354,11 @@ public class BatteryStatsImpl extends BatteryStats {
}
});
+ final long elapsedTimeMs = mClocks.uptimeMillis() - startTimeMs;
+ if (DEBUG_ENERGY_CPU || elapsedTimeMs >= 100) {
+ Slog.d(TAG, "Reading cpu freq times took " + elapsedTimeMs + "ms");
+ }
+
if (mWakeLockAllocationsUs != null) {
for (int i = 0; i < numWakelocks; ++i) {
final Uid u = partialTimers.get(i).mUid;
@@ -11224,7 +11584,9 @@ public class BatteryStatsImpl extends BatteryStats {
reportChangesToStatsLog(mHaveBatteryLevel ? mHistoryCur : null,
status, plugType, level, temp);
- final boolean onBattery = plugType == BATTERY_PLUGGED_NONE;
+ final boolean onBattery =
+ plugType == BATTERY_PLUGGED_NONE &&
+ status != BatteryManager.BATTERY_STATUS_UNKNOWN;
final long uptime = mClocks.uptimeMillis();
final long elapsedRealtime = mClocks.elapsedRealtime();
if (!mHaveBatteryLevel) {
@@ -11258,7 +11620,8 @@ public class BatteryStatsImpl extends BatteryStats {
mRecordingHistory = true;
startRecordingHistory(elapsedRealtime, uptime, true);
}
- } else if (level < 96) {
+ } else if (level < 96 &&
+ status != BatteryManager.BATTERY_STATUS_UNKNOWN) {
if (!mRecordingHistory) {
mRecordingHistory = true;
startRecordingHistory(elapsedRealtime, uptime, true);
@@ -11396,9 +11759,12 @@ public class BatteryStatsImpl extends BatteryStats {
addHistoryRecordLocked(elapsedRealtime, uptime);
}
}
- if (!onBattery && status == BatteryManager.BATTERY_STATUS_FULL) {
- // We don't record history while we are plugged in and fully charged.
- // The next time we are unplugged, history will be cleared.
+ if (!onBattery &&
+ (status == BatteryManager.BATTERY_STATUS_FULL ||
+ status == BatteryManager.BATTERY_STATUS_UNKNOWN)) {
+ // We don't record history while we are plugged in and fully charged
+ // (or when battery is not present). The next time we are
+ // unplugged, history will be cleared.
mRecordingHistory = DEBUG;
}
@@ -11581,6 +11947,51 @@ public class BatteryStatsImpl extends BatteryStats {
return (msPerLevel * (100-mCurrentBatteryLevel)) * 1000;
}
+ /*@hide */
+ public CellularBatteryStats getCellularBatteryStats() {
+ CellularBatteryStats s = new CellularBatteryStats();
+ final int which = STATS_SINCE_CHARGED;
+ final long rawRealTime = SystemClock.elapsedRealtime() * 1000;
+ final ControllerActivityCounter counter = getModemControllerActivity();
+ final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(which);
+ final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(which);
+ final long energyConsumedMaMs = counter.getPowerCounter().getCountLocked(which);
+ long[] timeInRatMs = new long[BatteryStats.NUM_DATA_CONNECTION_TYPES];
+ for (int i = 0; i < timeInRatMs.length; i++) {
+ timeInRatMs[i] = getPhoneDataConnectionTime(i, rawRealTime, which) / 1000;
+ }
+ long[] timeInRxSignalStrengthLevelMs = new long[SignalStrength.NUM_SIGNAL_STRENGTH_BINS];
+ for (int i = 0; i < timeInRxSignalStrengthLevelMs.length; i++) {
+ timeInRxSignalStrengthLevelMs[i]
+ = getPhoneSignalStrengthTime(i, rawRealTime, which) / 1000;
+ }
+ long[] txTimeMs = new long[Math.min(ModemActivityInfo.TX_POWER_LEVELS,
+ counter.getTxTimeCounters().length)];
+ long totalTxTimeMs = 0;
+ for (int i = 0; i < txTimeMs.length; i++) {
+ txTimeMs[i] = counter.getTxTimeCounters()[i].getCountLocked(which);
+ totalTxTimeMs += txTimeMs[i];
+ }
+ final long totalControllerActivityTimeMs
+ = computeBatteryRealtime(SystemClock.elapsedRealtime() * 1000, which) / 1000;
+ final long sleepTimeMs
+ = totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + totalTxTimeMs);
+ s.setLoggingDurationMs(computeBatteryRealtime(rawRealTime, which) / 1000);
+ s.setKernelActiveTimeMs(getMobileRadioActiveTime(rawRealTime, which) / 1000);
+ s.setNumPacketsTx(getNetworkActivityPackets(NETWORK_MOBILE_TX_DATA, which));
+ s.setNumBytesTx(getNetworkActivityBytes(NETWORK_MOBILE_TX_DATA, which));
+ s.setNumPacketsRx(getNetworkActivityPackets(NETWORK_MOBILE_RX_DATA, which));
+ s.setNumBytesRx(getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which));
+ s.setSleepTimeMs(sleepTimeMs);
+ s.setIdleTimeMs(idleTimeMs);
+ s.setRxTimeMs(rxTimeMs);
+ s.setEnergyConsumedMaMs(energyConsumedMaMs);
+ s.setTimeInRatMs(timeInRatMs);
+ s.setTimeInRxSignalStrengthLevelMs(timeInRxSignalStrengthLevelMs);
+ s.setTxTimeMs(txTimeMs);
+ return s;
+ }
+
@Override
public LevelStepTracker getChargeLevelStepTracker() {
return mChargeStepTracker;
@@ -11751,11 +12162,23 @@ public class BatteryStatsImpl extends BatteryStats {
return u;
}
+ /**
+ * Retrieve the statistics object for a particular uid. Returns null if the object is not
+ * available.
+ */
+ public Uid getAvailableUidStatsLocked(int uid) {
+ Uid u = mUidStats.get(uid);
+ return u;
+ }
+
public void onCleanupUserLocked(int userId) {
final int firstUidForUser = UserHandle.getUid(userId, 0);
final int lastUidForUser = UserHandle.getUid(userId, UserHandle.PER_USER_RANGE - 1);
mKernelUidCpuFreqTimeReader.removeUidsInRange(firstUidForUser, lastUidForUser);
mKernelUidCpuTimeReader.removeUidsInRange(firstUidForUser, lastUidForUser);
+ if (mKernelSingleUidTimeReader != null) {
+ mKernelSingleUidTimeReader.removeUidsInRange(firstUidForUser, lastUidForUser);
+ }
}
public void onUserRemovedLocked(int userId) {
@@ -11774,6 +12197,9 @@ public class BatteryStatsImpl extends BatteryStats {
public void removeUidStatsLocked(int uid) {
mKernelUidCpuTimeReader.removeUid(uid);
mKernelUidCpuFreqTimeReader.removeUid(uid);
+ if (mKernelSingleUidTimeReader != null) {
+ mKernelSingleUidTimeReader.removeUid(uid);
+ }
mUidStats.remove(uid);
}
@@ -12110,7 +12536,7 @@ public class BatteryStatsImpl extends BatteryStats {
PackageChange pc = new PackageChange();
pc.mPackageName = in.readString();
pc.mUpdate = in.readInt() != 0;
- pc.mVersionCode = in.readInt();
+ pc.mVersionCode = in.readLong();
mDailyPackageChanges.add(pc);
}
} else {
@@ -12375,6 +12801,28 @@ public class BatteryStatsImpl extends BatteryStats {
in, mOnBatteryTimeBase);
u.mScreenOffCpuFreqTimeMs = LongSamplingCounterArray.readSummaryFromParcelLocked(
in, mOnBatteryScreenOffTimeBase);
+ int length = in.readInt();
+ if (length == Uid.NUM_PROCESS_STATE) {
+ u.mProcStateTimeMs = new LongSamplingCounterArray[length];
+ for (int procState = 0; procState < length; ++procState) {
+ u.mProcStateTimeMs[procState]
+ = LongSamplingCounterArray.readSummaryFromParcelLocked(
+ in, mOnBatteryTimeBase);
+ }
+ } else {
+ u.mProcStateTimeMs = null;
+ }
+ length = in.readInt();
+ if (length == Uid.NUM_PROCESS_STATE) {
+ u.mProcStateScreenOffTimeMs = new LongSamplingCounterArray[length];
+ for (int procState = 0; procState < length; ++procState) {
+ u.mProcStateScreenOffTimeMs[procState]
+ = LongSamplingCounterArray.readSummaryFromParcelLocked(
+ in, mOnBatteryScreenOffTimeBase);
+ }
+ } else {
+ u.mProcStateScreenOffTimeMs = null;
+ }
if (in.readInt() != 0) {
u.mMobileRadioApWakeupCount = new LongSamplingCounter(mOnBatteryTimeBase);
@@ -12535,7 +12983,7 @@ public class BatteryStatsImpl extends BatteryStats {
PackageChange pc = mDailyPackageChanges.get(i);
out.writeString(pc.mPackageName);
out.writeInt(pc.mUpdate ? 1 : 0);
- out.writeInt(pc.mVersionCode);
+ out.writeLong(pc.mVersionCode);
}
} else {
out.writeInt(0);
@@ -12828,6 +13276,23 @@ public class BatteryStatsImpl extends BatteryStats {
LongSamplingCounterArray.writeSummaryToParcelLocked(out, u.mCpuFreqTimeMs);
LongSamplingCounterArray.writeSummaryToParcelLocked(out, u.mScreenOffCpuFreqTimeMs);
+ if (u.mProcStateTimeMs != null) {
+ out.writeInt(u.mProcStateTimeMs.length);
+ for (LongSamplingCounterArray counters : u.mProcStateTimeMs) {
+ LongSamplingCounterArray.writeSummaryToParcelLocked(out, counters);
+ }
+ } else {
+ out.writeInt(0);
+ }
+ if (u.mProcStateScreenOffTimeMs != null) {
+ out.writeInt(u.mProcStateScreenOffTimeMs.length);
+ for (LongSamplingCounterArray counters : u.mProcStateScreenOffTimeMs) {
+ LongSamplingCounterArray.writeSummaryToParcelLocked(out, counters);
+ }
+ } else {
+ out.writeInt(0);
+ }
+
if (u.mMobileRadioApWakeupCount != null) {
out.writeInt(1);
u.mMobileRadioApWakeupCount.writeSummaryFromParcelLocked(out);
diff --git a/com/android/internal/os/ByteTransferPipe.java b/com/android/internal/os/ByteTransferPipe.java
new file mode 100644
index 00000000..64898945
--- /dev/null
+++ b/com/android/internal/os/ByteTransferPipe.java
@@ -0,0 +1,49 @@
+/*
+ * 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 com.android.internal.os;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Helper class to get byte data through a pipe from a client app. Also {@see TransferPipe}.
+ */
+public class ByteTransferPipe extends TransferPipe {
+ static final String TAG = "ByteTransferPipe";
+
+ private ByteArrayOutputStream mOutputStream;
+
+ public ByteTransferPipe() throws IOException {
+ super();
+ }
+
+ public ByteTransferPipe(String bufferPrefix) throws IOException {
+ super(bufferPrefix, "ByteTransferPipe");
+ }
+
+ @Override
+ protected OutputStream getNewOutputStream() {
+ mOutputStream = new ByteArrayOutputStream();
+ return mOutputStream;
+ }
+
+ public byte[] get() throws IOException {
+ go(null);
+ return mOutputStream.toByteArray();
+ }
+}
diff --git a/com/android/internal/os/KernelSingleUidTimeReader.java b/com/android/internal/os/KernelSingleUidTimeReader.java
new file mode 100644
index 00000000..ca635a40
--- /dev/null
+++ b/com/android/internal/os/KernelSingleUidTimeReader.java
@@ -0,0 +1,204 @@
+/*
+ * 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 com.android.internal.os;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.annotation.NonNull;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+@VisibleForTesting(visibility = PACKAGE)
+public class KernelSingleUidTimeReader {
+ private final String TAG = KernelUidCpuFreqTimeReader.class.getName();
+ private final boolean DBG = false;
+
+ private final String PROC_FILE_DIR = "/proc/uid/";
+ private final String PROC_FILE_NAME = "/time_in_state";
+
+ @VisibleForTesting
+ public static final int TOTAL_READ_ERROR_COUNT = 5;
+
+ @GuardedBy("this")
+ private final int mCpuFreqsCount;
+
+ @GuardedBy("this")
+ private final SparseArray<long[]> mLastUidCpuTimeMs = new SparseArray<>();
+
+ @GuardedBy("this")
+ private int mReadErrorCounter;
+ @GuardedBy("this")
+ private boolean mSingleUidCpuTimesAvailable = true;
+
+ private final Injector mInjector;
+
+ KernelSingleUidTimeReader(int cpuFreqsCount) {
+ this(cpuFreqsCount, new Injector());
+ }
+
+ public KernelSingleUidTimeReader(int cpuFreqsCount, Injector injector) {
+ mInjector = injector;
+ mCpuFreqsCount = cpuFreqsCount;
+ if (mCpuFreqsCount == 0) {
+ mSingleUidCpuTimesAvailable = false;
+ }
+ }
+
+ public boolean singleUidCpuTimesAvailable() {
+ return mSingleUidCpuTimesAvailable;
+ }
+
+ public long[] readDeltaMs(int uid) {
+ synchronized (this) {
+ if (!mSingleUidCpuTimesAvailable) {
+ return null;
+ }
+ // Read total cpu times from the proc file.
+ final String procFile = new StringBuilder(PROC_FILE_DIR)
+ .append(uid)
+ .append(PROC_FILE_NAME).toString();
+ final long[] cpuTimesMs = new long[mCpuFreqsCount];
+ try {
+ final byte[] data = mInjector.readData(procFile);
+ final ByteBuffer buffer = ByteBuffer.wrap(data);
+ buffer.order(ByteOrder.nativeOrder());
+ for (int i = 0; i < mCpuFreqsCount; ++i) {
+ // Times read will be in units of 10ms
+ cpuTimesMs[i] = buffer.getLong() * 10;
+ }
+ } catch (Exception e) {
+ if (++mReadErrorCounter >= TOTAL_READ_ERROR_COUNT) {
+ mSingleUidCpuTimesAvailable = false;
+ }
+ if (DBG) Slog.e(TAG, "Some error occured while reading " + procFile, e);
+ return null;
+ }
+
+ return computeDelta(uid, cpuTimesMs);
+ }
+ }
+
+ /**
+ * Compute and return cpu times delta of an uid using previously read cpu times and
+ * {@param latestCpuTimesMs}.
+ *
+ * @return delta of cpu times if at least one of the cpu time at a freq is +ve, otherwise null.
+ */
+ public long[] computeDelta(int uid, @NonNull long[] latestCpuTimesMs) {
+ synchronized (this) {
+ if (!mSingleUidCpuTimesAvailable) {
+ return null;
+ }
+ // Subtract the last read cpu times to get deltas.
+ final long[] lastCpuTimesMs = mLastUidCpuTimeMs.get(uid);
+ final long[] deltaTimesMs = getDeltaLocked(lastCpuTimesMs, latestCpuTimesMs);
+ if (deltaTimesMs == null) {
+ if (DBG) Slog.e(TAG, "Malformed data read for uid=" + uid
+ + "; last=" + Arrays.toString(lastCpuTimesMs)
+ + "; latest=" + Arrays.toString(latestCpuTimesMs));
+ return null;
+ }
+ // If all elements are zero, return null to avoid unnecessary work on the caller side.
+ boolean hasNonZero = false;
+ for (int i = deltaTimesMs.length - 1; i >= 0; --i) {
+ if (deltaTimesMs[i] > 0) {
+ hasNonZero = true;
+ break;
+ }
+ }
+ if (hasNonZero) {
+ mLastUidCpuTimeMs.put(uid, latestCpuTimesMs);
+ return deltaTimesMs;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns null if the latest cpu times are not valid**, otherwise delta of
+ * {@param latestCpuTimesMs} and {@param lastCpuTimesMs}.
+ *
+ * **latest cpu times are considered valid if all the cpu times are +ve and
+ * greater than or equal to previously read cpu times.
+ */
+ @GuardedBy("this")
+ @VisibleForTesting(visibility = PACKAGE)
+ public long[] getDeltaLocked(long[] lastCpuTimesMs, @NonNull long[] latestCpuTimesMs) {
+ for (int i = latestCpuTimesMs.length - 1; i >= 0; --i) {
+ if (latestCpuTimesMs[i] < 0) {
+ return null;
+ }
+ }
+ if (lastCpuTimesMs == null) {
+ return latestCpuTimesMs;
+ }
+ final long[] deltaTimesMs = new long[latestCpuTimesMs.length];
+ for (int i = latestCpuTimesMs.length - 1; i >= 0; --i) {
+ deltaTimesMs[i] = latestCpuTimesMs[i] - lastCpuTimesMs[i];
+ if (deltaTimesMs[i] < 0) {
+ return null;
+ }
+ }
+ return deltaTimesMs;
+ }
+
+ public void removeUid(int uid) {
+ synchronized (this) {
+ mLastUidCpuTimeMs.delete(uid);
+ }
+ }
+
+ public void removeUidsInRange(int startUid, int endUid) {
+ if (endUid < startUid) {
+ return;
+ }
+ synchronized (this) {
+ mLastUidCpuTimeMs.put(startUid, null);
+ mLastUidCpuTimeMs.put(endUid, null);
+ final int startIdx = mLastUidCpuTimeMs.indexOfKey(startUid);
+ final int endIdx = mLastUidCpuTimeMs.indexOfKey(endUid);
+ mLastUidCpuTimeMs.removeAtRange(startIdx, endIdx - startIdx + 1);
+ }
+ }
+
+ @VisibleForTesting
+ public static class Injector {
+ public byte[] readData(String procFile) throws IOException {
+ return Files.readAllBytes(Paths.get(procFile));
+ }
+ }
+
+ @VisibleForTesting
+ public SparseArray<long[]> getLastUidCpuTimeMs() {
+ return mLastUidCpuTimeMs;
+ }
+
+ @VisibleForTesting
+ public void setSingleUidCpuTimesAvailable(boolean singleUidCpuTimesAvailable) {
+ mSingleUidCpuTimesAvailable = singleUidCpuTimesAvailable;
+ }
+} \ No newline at end of file
diff --git a/com/android/internal/os/KernelUidCpuFreqTimeReader.java b/com/android/internal/os/KernelUidCpuFreqTimeReader.java
index a39997d3..b8982cce 100644
--- a/com/android/internal/os/KernelUidCpuFreqTimeReader.java
+++ b/com/android/internal/os/KernelUidCpuFreqTimeReader.java
@@ -66,13 +66,21 @@ public class KernelUidCpuFreqTimeReader {
// start reading) and if it is not available, we simply ignore further read requests.
private static final int TOTAL_READ_ERROR_COUNT = 5;
private int mReadErrorCounter;
- private boolean mProcFileAvailable;
private boolean mPerClusterTimesAvailable;
+ private boolean mAllUidTimesAvailable = true;
public boolean perClusterTimesAvailable() {
return mPerClusterTimesAvailable;
}
+ public boolean allUidTimesAvailable() {
+ return mAllUidTimesAvailable;
+ }
+
+ public SparseArray<long[]> getAllUidCpuFreqTimeMs() {
+ return mLastUidCpuFreqTimeMs;
+ }
+
public long[] readFreqs(@NonNull PowerProfile powerProfile) {
checkNotNull(powerProfile);
@@ -80,15 +88,16 @@ public class KernelUidCpuFreqTimeReader {
// No need to read cpu freqs more than once.
return mCpuFreqs;
}
- if (!mProcFileAvailable && mReadErrorCounter >= TOTAL_READ_ERROR_COUNT) {
+ if (!mAllUidTimesAvailable) {
return null;
}
final int oldMask = StrictMode.allowThreadDiskReadsMask();
try (BufferedReader reader = new BufferedReader(new FileReader(UID_TIMES_PROC_FILE))) {
- mProcFileAvailable = true;
return readFreqs(reader, powerProfile);
} catch (IOException e) {
- mReadErrorCounter++;
+ if (++mReadErrorCounter >= TOTAL_READ_ERROR_COUNT) {
+ mAllUidTimesAvailable = false;
+ }
Slog.e(TAG, "Failed to read " + UID_TIMES_PROC_FILE + ": " + e);
return null;
} finally {
@@ -107,7 +116,7 @@ public class KernelUidCpuFreqTimeReader {
}
public void readDelta(@Nullable Callback callback) {
- if (!mProcFileAvailable) {
+ if (mCpuFreqs == null) {
return;
}
final int oldMask = StrictMode.allowThreadDiskReadsMask();
diff --git a/com/android/internal/os/SomeArgs.java b/com/android/internal/os/SomeArgs.java
index 8fb56d47..d9aa3253 100644
--- a/com/android/internal/os/SomeArgs.java
+++ b/com/android/internal/os/SomeArgs.java
@@ -48,6 +48,7 @@ public final class SomeArgs {
public Object arg6;
public Object arg7;
public Object arg8;
+ public Object arg9;
public int argi1;
public int argi2;
public int argi3;
diff --git a/com/android/internal/os/TransferPipe.java b/com/android/internal/os/TransferPipe.java
index 738ecc0b..1c09bd6a 100644
--- a/com/android/internal/os/TransferPipe.java
+++ b/com/android/internal/os/TransferPipe.java
@@ -34,11 +34,12 @@ import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.OutputStream;
/**
* Helper for transferring data through a pipe from a client app.
*/
-public final class TransferPipe implements Runnable, Closeable {
+public class TransferPipe implements Runnable, Closeable {
static final String TAG = "TransferPipe";
static final boolean DEBUG = false;
@@ -64,7 +65,11 @@ public final class TransferPipe implements Runnable, Closeable {
}
public TransferPipe(String bufferPrefix) throws IOException {
- mThread = new Thread(this, "TransferPipe");
+ this(bufferPrefix, "TransferPipe");
+ }
+
+ protected TransferPipe(String bufferPrefix, String threadName) throws IOException {
+ mThread = new Thread(this, threadName);
mFds = ParcelFileDescriptor.createPipe();
mBufferPrefix = bufferPrefix;
}
@@ -234,11 +239,15 @@ public final class TransferPipe implements Runnable, Closeable {
}
}
+ protected OutputStream getNewOutputStream() {
+ return new FileOutputStream(mOutFd);
+ }
+
@Override
public void run() {
final byte[] buffer = new byte[1024];
final FileInputStream fis;
- final FileOutputStream fos;
+ final OutputStream fos;
synchronized (this) {
ParcelFileDescriptor readFd = getReadFd();
@@ -247,7 +256,7 @@ public final class TransferPipe implements Runnable, Closeable {
return;
}
fis = new FileInputStream(readFd.getFileDescriptor());
- fos = new FileOutputStream(mOutFd);
+ fos = getNewOutputStream();
}
if (DEBUG) Slog.i(TAG, "Ready to read pipe...");
diff --git a/com/android/internal/telephony/BaseCommands.java b/com/android/internal/telephony/BaseCommands.java
index 9304f910..b70e800c 100644
--- a/com/android/internal/telephony/BaseCommands.java
+++ b/com/android/internal/telephony/BaseCommands.java
@@ -45,6 +45,7 @@ public abstract class BaseCommands implements CommandsInterface {
protected RegistrantList mVoiceRadioTechChangedRegistrants = new RegistrantList();
protected RegistrantList mImsNetworkStateChangedRegistrants = new RegistrantList();
protected RegistrantList mIccStatusChangedRegistrants = new RegistrantList();
+ protected RegistrantList mIccSlotStatusChangedRegistrants = new RegistrantList();
protected RegistrantList mVoicePrivacyOnRegistrants = new RegistrantList();
protected RegistrantList mVoicePrivacyOffRegistrants = new RegistrantList();
protected RegistrantList mOtaProvisionRegistrants = new RegistrantList();
@@ -282,6 +283,17 @@ public abstract class BaseCommands implements CommandsInterface {
}
@Override
+ public void registerForIccSlotStatusChanged(Handler h, int what, Object obj) {
+ Registrant r = new Registrant(h, what, obj);
+ mIccSlotStatusChangedRegistrants.add(r);
+ }
+
+ @Override
+ public void unregisterForIccSlotStatusChanged(Handler h) {
+ mIccSlotStatusChangedRegistrants.remove(h);
+ }
+
+ @Override
public void setOnNewGsmSms(Handler h, int what, Object obj) {
mGsmSmsRegistrant = new Registrant (h, what, obj);
}
diff --git a/com/android/internal/telephony/CarrierActionAgent.java b/com/android/internal/telephony/CarrierActionAgent.java
index 6b9a70af..41eebbfe 100644
--- a/com/android/internal/telephony/CarrierActionAgent.java
+++ b/com/android/internal/telephony/CarrierActionAgent.java
@@ -33,6 +33,7 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -147,7 +148,7 @@ public class CarrierActionAgent extends Handler {
break;
case EVENT_MOBILE_DATA_SETTINGS_CHANGED:
log("EVENT_MOBILE_DATA_SETTINGS_CHANGED");
- if (!mPhone.getDataEnabled()) carrierActionReset();
+ if (!mPhone.isUserDataEnabled()) carrierActionReset();
break;
case EVENT_DATA_ROAMING_OFF:
log("EVENT_DATA_ROAMING_OFF");
diff --git a/com/android/internal/telephony/CarrierIdentifier.java b/com/android/internal/telephony/CarrierIdentifier.java
index e8be159b..5a700aef 100644
--- a/com/android/internal/telephony/CarrierIdentifier.java
+++ b/com/android/internal/telephony/CarrierIdentifier.java
@@ -15,7 +15,10 @@
*/
package com.android.internal.telephony;
+import static android.provider.Telephony.CarrierIdentification;
+
import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.database.Cursor;
@@ -41,8 +44,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
-import static android.provider.Telephony.CarrierIdentification;
-
/**
* CarrierIdentifier identifies the subscription carrier and returns a canonical carrier Id
* and a user friendly carrier name. CarrierIdentifier reads subscription info and check against
@@ -65,12 +66,11 @@ public class CarrierIdentifier extends Handler {
private static final Uri CONTENT_URL_PREFER_APN = Uri.withAppendedPath(
Telephony.Carriers.CONTENT_URI, "preferapn");
private static final String OPERATOR_BRAND_OVERRIDE_PREFIX = "operator_branding_";
- private static final int INVALID_CARRIER_ID = -1;
// cached matching rules based mccmnc to speed up resolution
private List<CarrierMatchingRule> mCarrierMatchingRulesOnMccMnc = new ArrayList<>();
// cached carrier Id
- private int mCarrierId = INVALID_CARRIER_ID;
+ private int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
// cached carrier name
private String mCarrierName;
// cached preferapn name
@@ -200,7 +200,7 @@ public class CarrierIdentifier extends Handler {
mCarrierMatchingRulesOnMccMnc.clear();
mSpn = null;
mPreferApn = null;
- updateCarrierIdAndName(INVALID_CARRIER_ID, null);
+ updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null);
break;
case PREFER_APN_UPDATE_EVENT:
String preferApn = getPreferApn();
@@ -309,9 +309,14 @@ public class CarrierIdentifier extends Handler {
update = true;
}
if (update) {
- // TODO new public intent CARRIER_ID_CHANGED
mCarrierIdLocalLog.log("[updateCarrierIdAndName] cid:" + mCarrierId + " name:"
+ mCarrierName);
+ final Intent intent = new Intent(TelephonyManager
+ .ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED);
+ intent.putExtra(TelephonyManager.EXTRA_CARRIER_ID, mCarrierId);
+ intent.putExtra(TelephonyManager.EXTRA_CARRIER_NAME, mCarrierName);
+ intent.putExtra(TelephonyManager.EXTRA_SUBSCRIPTION_ID, mPhone.getSubId());
+ mContext.sendBroadcast(intent);
}
}
@@ -320,6 +325,8 @@ public class CarrierIdentifier extends Handler {
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.MCCMNC)),
cursor.getString(cursor.getColumnIndexOrThrow(
CarrierIdentification.IMSI_PREFIX_XPATTERN)),
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ CarrierIdentification.ICCID_PREFIX)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.GID1)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.GID2)),
cursor.getString(cursor.getColumnIndexOrThrow(CarrierIdentification.PLMN)),
@@ -342,8 +349,9 @@ public class CarrierIdentifier extends Handler {
* rule 1 {mccmnc, imsi} rule 2 {mccmnc, imsi, gid1} and rule 3 {mccmnc, imsi, gid2} all
* matches with subscription data. rule 2 wins with the highest matching score.
*/
- private static final int SCORE_MCCMNC = 1 << 6;
- private static final int SCORE_IMSI_PREFIX = 1 << 5;
+ private static final int SCORE_MCCMNC = 1 << 7;
+ private static final int SCORE_IMSI_PREFIX = 1 << 6;
+ private static final int SCORE_ICCID_PREFIX = 1 << 5;
private static final int SCORE_GID1 = 1 << 4;
private static final int SCORE_GID2 = 1 << 3;
private static final int SCORE_PLMN = 1 << 2;
@@ -355,6 +363,7 @@ public class CarrierIdentifier extends Handler {
// carrier matching attributes
private String mMccMnc;
private String mImsiPrefixPattern;
+ private String mIccidPrefix;
private String mGid1;
private String mGid2;
private String mPlmn;
@@ -368,10 +377,12 @@ public class CarrierIdentifier extends Handler {
private int mScore = 0;
- CarrierMatchingRule(String mccmnc, String imsiPrefixPattern, String gid1, String gid2,
- String plmn, String spn, String apn, int cid, String name) {
+ CarrierMatchingRule(String mccmnc, String imsiPrefixPattern, String iccidPrefix,
+ String gid1, String gid2, String plmn, String spn, String apn, int cid,
+ String name) {
mMccMnc = mccmnc;
mImsiPrefixPattern = imsiPrefixPattern;
+ mIccidPrefix = iccidPrefix;
mGid1 = gid1;
mGid2 = gid2;
mPlmn = plmn;
@@ -383,9 +394,9 @@ public class CarrierIdentifier extends Handler {
// Calculate matching score. Values which aren't set in the rule are considered "wild".
// All values in the rule must match in order for the subscription to be considered part of
- // the carrier. otherwise, a invalid score -1 will be assigned. A match from a higher tier
+ // the carrier. Otherwise, a invalid score -1 will be assigned. A match from a higher tier
// will beat any subsequent match which does not match at that tier. When there are multiple
- // matches at the same tier, the longest, best match will be used.
+ // matches at the same tier, the match with highest score will be used.
public void match(CarrierMatchingRule subscriptionRule) {
mScore = 0;
if (mMccMnc != null) {
@@ -402,6 +413,13 @@ public class CarrierIdentifier extends Handler {
}
mScore += SCORE_IMSI_PREFIX;
}
+ if (mIccidPrefix != null) {
+ if (!iccidPrefixMatch(subscriptionRule.mIccidPrefix, mIccidPrefix)) {
+ mScore = SCORE_INVALID;
+ return;
+ }
+ mScore += SCORE_ICCID_PREFIX;
+ }
if (mGid1 != null) {
// full string match. carrier matching should cover the corner case that gid1
// with garbage tail due to SIM manufacture issues.
@@ -458,6 +476,13 @@ public class CarrierIdentifier extends Handler {
return true;
}
+ private boolean iccidPrefixMatch(String iccid, String prefix) {
+ if (iccid == null || prefix == null) {
+ return false;
+ }
+ return iccid.startsWith(prefix);
+ }
+
public String toString() {
return "[CarrierMatchingRule] -"
+ " mccmnc: " + mMccMnc
@@ -465,6 +490,7 @@ public class CarrierIdentifier extends Handler {
+ " gid2: " + mGid2
+ " plmn: " + mPlmn
+ " imsi_prefix: " + mImsiPrefixPattern
+ + " iccid_prefix" + mIccidPrefix
+ " spn: " + mSpn
+ " apn: " + mApn
+ " name: " + mName
@@ -483,6 +509,7 @@ public class CarrierIdentifier extends Handler {
return;
}
final String mccmnc = mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId());
+ final String iccid = mPhone.getIccSerialNumber();
final String gid1 = mPhone.getGroupIdLevel1();
final String gid2 = mPhone.getGroupIdLevel2();
final String imsi = mPhone.getSubscriberId();
@@ -495,13 +522,15 @@ public class CarrierIdentifier extends Handler {
+ " gid1: " + gid1
+ " gid2: " + gid2
+ " imsi: " + Rlog.pii(LOG_TAG, imsi)
+ + " iccid: " + Rlog.pii(LOG_TAG, iccid)
+ " plmn: " + plmn
+ " spn: " + spn
+ " apn: " + apn);
}
CarrierMatchingRule subscriptionRule = new CarrierMatchingRule(
- mccmnc, imsi, gid1, gid2, plmn, spn, apn, INVALID_CARRIER_ID, null);
+ mccmnc, imsi, iccid, gid1, gid2, plmn, spn, apn,
+ TelephonyManager.UNKNOWN_CARRIER_ID, null);
int maxScore = CarrierMatchingRule.SCORE_INVALID;
CarrierMatchingRule maxRule = null;
@@ -514,8 +543,9 @@ public class CarrierIdentifier extends Handler {
}
}
if (maxScore == CarrierMatchingRule.SCORE_INVALID) {
- logd("[matchCarrier - no match] cid: " + INVALID_CARRIER_ID + " name: " + null);
- updateCarrierIdAndName(INVALID_CARRIER_ID, null);
+ logd("[matchCarrier - no match] cid: " + TelephonyManager.UNKNOWN_CARRIER_ID
+ + " name: " + null);
+ updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null);
} else {
logd("[matchCarrier] cid: " + maxRule.mCid + " name: " + maxRule.mName);
updateCarrierIdAndName(maxRule.mCid, maxRule.mName);
diff --git a/com/android/internal/telephony/CommandException.java b/com/android/internal/telephony/CommandException.java
index 6cba8f26..1d2cc3a7 100644
--- a/com/android/internal/telephony/CommandException.java
+++ b/com/android/internal/telephony/CommandException.java
@@ -16,8 +16,6 @@
package com.android.internal.telephony;
-import com.android.internal.telephony.RILConstants;
-
import android.telephony.Rlog;
/**
@@ -52,6 +50,7 @@ public class CommandException extends RuntimeException {
USSD_MODIFIED_TO_SS,
USSD_MODIFIED_TO_USSD,
SS_MODIFIED_TO_DIAL,
+ SS_MODIFIED_TO_DIAL_VIDEO,
SS_MODIFIED_TO_USSD,
SS_MODIFIED_TO_SS,
SIM_ALREADY_POWERED_OFF,
diff --git a/com/android/internal/telephony/CommandsInterface.java b/com/android/internal/telephony/CommandsInterface.java
index d5eb5463..3731e02e 100644
--- a/com/android/internal/telephony/CommandsInterface.java
+++ b/com/android/internal/telephony/CommandsInterface.java
@@ -189,6 +189,10 @@ public interface CommandsInterface {
*/
void registerForIccStatusChanged(Handler h, int what, Object obj);
void unregisterForIccStatusChanged(Handler h);
+ /** Register for ICC slot status changed event */
+ void registerForIccSlotStatusChanged(Handler h, int what, Object obj);
+ /** Unregister for ICC slot status changed event */
+ void unregisterForIccSlotStatusChanged(Handler h);
void registerForCallStateChanged(Handler h, int what, Object obj);
void unregisterForCallStateChanged(Handler h);
@@ -1692,6 +1696,22 @@ public interface CommandsInterface {
public void getIccCardStatus(Message result);
/**
+ * Request the status of all the physical UICC slots.
+ *
+ * @param result Callback message containing a {@link java.util.ArrayList} of
+ * {@link com.android.internal.telephony.uicc.IccSlotStatus} instances for all the slots.
+ */
+ void getIccSlotsStatus(Message result);
+
+ /**
+ * Set the mapping from logical slots to physical slots.
+ *
+ * @param physicalSlots Mapping from logical slots to physical slots.
+ * @param result Callback message is empty on completion.
+ */
+ void setLogicalToPhysicalSlotMapping(int[] physicalSlots, Message result);
+
+ /**
* Return if the current radio is LTE on CDMA. This
* is a tri-state return value as for a period of time
* the mode may be unknown.
diff --git a/com/android/internal/telephony/GsmCdmaPhone.java b/com/android/internal/telephony/GsmCdmaPhone.java
index 64804177..27364985 100644
--- a/com/android/internal/telephony/GsmCdmaPhone.java
+++ b/com/android/internal/telephony/GsmCdmaPhone.java
@@ -1076,7 +1076,7 @@ public class GsmCdmaPhone extends Phone {
boolean useImsForEmergency = imsPhone != null
&& isEmergency
&& alwaysTryImsForEmergencyCarrierConfig
- && ImsManager.isNonTtyOrTtyOnVolteEnabled(mContext)
+ && ImsManager.getInstance(mContext, mPhoneId).isNonTtyOrTtyOnVolteEnabled()
&& imsPhone.isImsAvailable();
String dialPart = PhoneNumberUtils.extractNetworkPortionAlt(PhoneNumberUtils.
@@ -1102,7 +1102,7 @@ public class GsmCdmaPhone extends Phone {
+ ((imsPhone != null) ? imsPhone.getServiceState().getState() : "N/A"));
}
- Phone.checkWfcWifiOnlyModeBeforeDial(mImsPhone, mContext);
+ Phone.checkWfcWifiOnlyModeBeforeDial(mImsPhone, mPhoneId, mContext);
if ((useImsForCall && !isUt) || (isUt && useImsForUt) || useImsForEmergency) {
try {
@@ -1777,7 +1777,8 @@ public class GsmCdmaPhone extends Phone {
if (isPhoneTypeGsm()) {
Phone imsPhone = mImsPhone;
if ((imsPhone != null)
- && (imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)) {
+ && ((imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)
+ || imsPhone.isUtEnabled())) {
imsPhone.getOutgoingCallerIdDisplay(onComplete);
return;
}
@@ -1792,7 +1793,8 @@ public class GsmCdmaPhone extends Phone {
if (isPhoneTypeGsm()) {
Phone imsPhone = mImsPhone;
if ((imsPhone != null)
- && (imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)) {
+ && ((imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE)
+ || imsPhone.isUtEnabled())) {
imsPhone.setOutgoingCallerIdDisplay(commandInterfaceCLIRMode, onComplete);
return;
}
@@ -1980,13 +1982,18 @@ public class GsmCdmaPhone extends Phone {
}
@Override
- public boolean getDataEnabled() {
- return mDcTracker.getDataEnabled();
+ public boolean isUserDataEnabled() {
+ return mDcTracker.isUserDataEnabled();
+ }
+
+ @Override
+ public boolean isDataEnabled() {
+ return mDcTracker.isDataEnabled();
}
@Override
- public void setDataEnabled(boolean enable) {
- mDcTracker.setDataEnabled(enable);
+ public void setUserDataEnabled(boolean enable) {
+ mDcTracker.setUserDataEnabled(enable);
}
/**
@@ -2243,7 +2250,7 @@ public class GsmCdmaPhone extends Phone {
mCi.getVoiceRadioTechnology(obtainMessage(EVENT_REQUEST_VOICE_RADIO_TECH_DONE));
}
// Force update IMS service
- ImsManager.updateImsServiceConfig(mContext, mPhoneId, true);
+ ImsManager.getInstance(mContext, mPhoneId).updateImsServiceConfig(true);
// Update broadcastEmergencyCallStateChanges
CarrierConfigManager configMgr = (CarrierConfigManager)
diff --git a/com/android/internal/telephony/IccCard.java b/com/android/internal/telephony/IccCard.java
index e5b34e29..272a1a61 100644
--- a/com/android/internal/telephony/IccCard.java
+++ b/com/android/internal/telephony/IccCard.java
@@ -55,24 +55,12 @@ public interface IccCard {
public IccFileHandler getIccFileHandler();
/**
- * Notifies handler of any transition into IccCardConstants.State.ABSENT
- */
- public void registerForAbsent(Handler h, int what, Object obj);
- public void unregisterForAbsent(Handler h);
-
- /**
* Notifies handler of any transition into IccCardConstants.State.NETWORK_LOCKED
*/
public void registerForNetworkLocked(Handler h, int what, Object obj);
public void unregisterForNetworkLocked(Handler h);
/**
- * Notifies handler of any transition into IccCardConstants.State.isPinLocked()
- */
- public void registerForLocked(Handler h, int what, Object obj);
- public void unregisterForLocked(Handler h);
-
- /**
* Supply the ICC PIN to the ICC
*
* When the operation is complete, onComplete will be sent to its
diff --git a/com/android/internal/telephony/IccSmsInterfaceManager.java b/com/android/internal/telephony/IccSmsInterfaceManager.java
index 997ccea3..0fc08c65 100644
--- a/com/android/internal/telephony/IccSmsInterfaceManager.java
+++ b/com/android/internal/telephony/IccSmsInterfaceManager.java
@@ -78,8 +78,6 @@ public class IccSmsInterfaceManager {
protected static final int EVENT_SET_BROADCAST_CONFIG_DONE = 4;
private static final int SMS_CB_CODE_SCHEME_MIN = 0;
private static final int SMS_CB_CODE_SCHEME_MAX = 255;
- public static final int SMS_MESSAGE_PRIORITY_NOT_SPECIFIED = -1;
- public static final int SMS_MESSAGE_PERIOD_NOT_SPECIFIED = -1;
protected Phone mPhone;
final protected Context mContext;
@@ -395,8 +393,7 @@ public class IccSmsInterfaceManager {
Manifest.permission.SEND_SMS,
"Sending SMS message");
sendTextInternal(callingPackage, destAddr, scAddr, text, sentIntent, deliveryIntent,
- persistMessageForNonDefaultSmsApp, SMS_MESSAGE_PRIORITY_NOT_SPECIFIED,
- false /* expectMore */, SMS_MESSAGE_PERIOD_NOT_SPECIFIED);
+ persistMessageForNonDefaultSmsApp);
}
/**
@@ -410,8 +407,7 @@ public class IccSmsInterfaceManager {
Manifest.permission.SEND_SMS,
"Sending SMS message");
sendTextInternal(callingPackage, destAddr, scAddr, text, sentIntent, deliveryIntent,
- persistMessage, SMS_MESSAGE_PRIORITY_NOT_SPECIFIED, false /* expectMore */,
- SMS_MESSAGE_PERIOD_NOT_SPECIFIED);
+ persistMessage);
}
/**
@@ -437,39 +433,15 @@ public class IccSmsInterfaceManager {
* @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 persistMessageForNonDefaultSmsApp whether the sent message should
- * be automatically persisted in the SMS db. It only affects messages sent
- * by a non-default SMS app. Currently only the carrier app can set this
- * parameter to false to skip auto message persistence.
- * @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 including 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 including negative considered as Invalid Validity Period of the message.
*/
private void sendTextInternal(String callingPackage, String destAddr, String scAddr,
String text, PendingIntent sentIntent, PendingIntent deliveryIntent,
- boolean persistMessageForNonDefaultSmsApp, int priority, boolean expectMore,
- int validityPeriod) {
+ boolean persistMessageForNonDefaultSmsApp) {
if (Rlog.isLoggable("SMS", Log.VERBOSE)) {
log("sendText: destAddr=" + destAddr + " scAddr=" + scAddr +
" text='"+ text + "' sentIntent=" +
- sentIntent + " deliveryIntent=" + deliveryIntent
- + " priority=" + priority + " expectMore=" + expectMore
- + " validityPeriod=" + validityPeriod);
+ sentIntent + " deliveryIntent=" + deliveryIntent);
}
if (mAppOps.noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(),
callingPackage) != AppOpsManager.MODE_ALLOWED) {
@@ -480,65 +452,7 @@ public class IccSmsInterfaceManager {
}
destAddr = filterDestAddress(destAddr);
mDispatcher.sendText(destAddr, scAddr, text, sentIntent, deliveryIntent,
- null/*messageUri*/, callingPackage, persistMessageForNonDefaultSmsApp,
- priority, expectMore, validityPeriod);
- }
-
- /**
- * Send a text based SMS with Messaging Options.
- *
- * @param destAddr the address to send the message to
- * @param scAddr 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 persistMessageForNonDefaultSmsApp whether the sent message should
- * be automatically persisted in the SMS db. It only affects messages sent
- * by a non-default SMS app. Currently only the carrier app can set this
- * parameter to false to skip auto message persistence.
- * @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 including 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 including negative considered as Invalid Validity Period of the message.
- */
-
- public void sendTextWithOptions(String callingPackage, String destAddr, String scAddr,
- String text, PendingIntent sentIntent, PendingIntent deliveryIntent,
- boolean persistMessageForNonDefaultSmsApp, int priority, boolean expectMore,
- int validityPeriod) {
- mPhone.getContext().enforceCallingOrSelfPermission(
- Manifest.permission.SEND_SMS,
- "Sending SMS message");
- sendTextInternal(callingPackage, destAddr, scAddr, text, sentIntent, deliveryIntent,
- persistMessageForNonDefaultSmsApp, priority, expectMore, validityPeriod);
+ null/*messageUri*/, callingPackage, persistMessageForNonDefaultSmsApp);
}
/**
@@ -590,63 +504,6 @@ public class IccSmsInterfaceManager {
public void sendMultipartText(String callingPackage, String destAddr, String scAddr,
List<String> parts, List<PendingIntent> sentIntents,
List<PendingIntent> deliveryIntents, boolean persistMessageForNonDefaultSmsApp) {
- sendMultipartTextWithOptions(callingPackage, destAddr, destAddr, parts, sentIntents,
- deliveryIntents, persistMessageForNonDefaultSmsApp,
- SMS_MESSAGE_PRIORITY_NOT_SPECIFIED, false /* expectMore */,
- SMS_MESSAGE_PERIOD_NOT_SPECIFIED);
- }
-
- /**
- * Send a multi-part text based SMS with Messaging Options.
- *
- * @param destAddr the address to send the message to
- * @param scAddr 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:
- * <code>RESULT_ERROR_GENERIC_FAILURE</code>
- * <code>RESULT_ERROR_RADIO_OFF</code>
- * <code>RESULT_ERROR_NULL_PDU</code>.
- * 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 persistMessageForNonDefaultSmsApp whether the sent message should
- * be automatically persisted in the SMS db. It only affects messages sent
- * by a non-default SMS app. Currently only the carrier app can set this
- * parameter to false to skip auto message persistence.
- * @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 including 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 including negative considered as Invalid Validity Period of the message.
- */
-
- public void sendMultipartTextWithOptions(String callingPackage, String destAddr,
- String scAddr, List<String> parts, List<PendingIntent> sentIntents,
- List<PendingIntent> deliveryIntents, boolean persistMessageForNonDefaultSmsApp,
- int priority, boolean expectMore, int validityPeriod) {
mPhone.getContext().enforceCallingPermission(
Manifest.permission.SEND_SMS,
"Sending SMS message");
@@ -657,7 +514,7 @@ public class IccSmsInterfaceManager {
if (Rlog.isLoggable("SMS", Log.VERBOSE)) {
int i = 0;
for (String part : parts) {
- log("sendMultipartTextWithOptions: destAddr=" + destAddr + ", srAddr=" + scAddr +
+ log("sendMultipartText: destAddr=" + destAddr + ", srAddr=" + scAddr +
", part[" + (i++) + "]=" + part);
}
}
@@ -692,21 +549,17 @@ public class IccSmsInterfaceManager {
mDispatcher.sendText(destAddr, scAddr, singlePart,
singleSentIntent, singleDeliveryIntent,
null/*messageUri*/, callingPackage,
- persistMessageForNonDefaultSmsApp,
- priority, expectMore, validityPeriod);
+ persistMessageForNonDefaultSmsApp);
}
return;
}
- mDispatcher.sendMultipartText(destAddr,
- scAddr,
- (ArrayList<String>) parts,
- (ArrayList<PendingIntent>) sentIntents,
- (ArrayList<PendingIntent>) deliveryIntents,
- null, callingPackage, persistMessageForNonDefaultSmsApp,
- priority, expectMore, validityPeriod);
+ mDispatcher.sendMultipartText(destAddr, scAddr, (ArrayList<String>) parts,
+ (ArrayList<PendingIntent>) sentIntents, (ArrayList<PendingIntent>) deliveryIntents,
+ null/*messageUri*/, callingPackage, persistMessageForNonDefaultSmsApp);
}
+
public int getPremiumSmsPermission(String packageName) {
return mDispatcher.getPremiumSmsPermission(packageName);
}
@@ -1100,8 +953,7 @@ public class IccSmsInterfaceManager {
textAndAddress[1] = filterDestAddress(textAndAddress[1]);
mDispatcher.sendText(textAndAddress[1], scAddress, textAndAddress[0],
sentIntent, deliveryIntent, messageUri, callingPkg,
- true /* persistMessageForNonDefaultSmsApp */, SMS_MESSAGE_PRIORITY_NOT_SPECIFIED,
- false /* expectMore */, SMS_MESSAGE_PERIOD_NOT_SPECIFIED);
+ true /* persistMessageForNonDefaultSmsApp */);
}
public void sendStoredMultipartText(String callingPkg, Uri messageUri, String scAddress,
@@ -1157,9 +1009,7 @@ public class IccSmsInterfaceManager {
mDispatcher.sendText(textAndAddress[1], scAddress, singlePart,
singleSentIntent, singleDeliveryIntent, messageUri, callingPkg,
- true /* persistMessageForNonDefaultSmsApp */,
- SMS_MESSAGE_PRIORITY_NOT_SPECIFIED,
- false /* expectMore */, SMS_MESSAGE_PERIOD_NOT_SPECIFIED);
+ true /* persistMessageForNonDefaultSmsApp */);
}
return;
}
@@ -1172,10 +1022,7 @@ public class IccSmsInterfaceManager {
(ArrayList<PendingIntent>) deliveryIntents,
messageUri,
callingPkg,
- true /* persistMessageForNonDefaultSmsApp */,
- SMS_MESSAGE_PRIORITY_NOT_SPECIFIED,
- false /* expectMore */,
- SMS_MESSAGE_PERIOD_NOT_SPECIFIED);
+ true /* persistMessageForNonDefaultSmsApp */);
}
private boolean isFailedOrDraft(ContentResolver resolver, Uri messageUri) {
diff --git a/com/android/internal/telephony/ImsSMSDispatcher.java b/com/android/internal/telephony/ImsSMSDispatcher.java
index bc829d61..4d8f62c9 100644
--- a/com/android/internal/telephony/ImsSMSDispatcher.java
+++ b/com/android/internal/telephony/ImsSMSDispatcher.java
@@ -173,15 +173,13 @@ public class ImsSMSDispatcher extends SMSDispatcher {
public void sendMultipartText(String destAddr, String scAddr,
ArrayList<String> parts, ArrayList<PendingIntent> sentIntents,
ArrayList<PendingIntent> deliveryIntents, Uri messageUri, String callingPkg,
- boolean persistMessage, int priority, boolean expectMore, int validityPeriod) {
+ boolean persistMessage) {
if (isCdmaMo()) {
mCdmaDispatcher.sendMultipartText(destAddr, scAddr,
- parts, sentIntents, deliveryIntents, messageUri, callingPkg, persistMessage,
- priority, expectMore, validityPeriod);
+ parts, sentIntents, deliveryIntents, messageUri, callingPkg, persistMessage);
} else {
mGsmDispatcher.sendMultipartText(destAddr, scAddr,
- parts, sentIntents, deliveryIntents, messageUri, callingPkg, persistMessage,
- priority, expectMore, validityPeriod);
+ parts, sentIntents, deliveryIntents, messageUri, callingPkg, persistMessage);
}
}
@@ -201,16 +199,14 @@ public class ImsSMSDispatcher extends SMSDispatcher {
@Override
public void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent,
PendingIntent deliveryIntent, Uri messageUri, String callingPkg,
- boolean persistMessage, int priority, boolean expectMore, int validityPeriod) {
+ boolean persistMessage) {
Rlog.d(TAG, "sendText");
if (isCdmaMo()) {
mCdmaDispatcher.sendText(destAddr, scAddr,
- text, sentIntent, deliveryIntent, messageUri, callingPkg, persistMessage,
- priority, expectMore, validityPeriod);
+ text, sentIntent, deliveryIntent, messageUri, callingPkg, persistMessage);
} else {
mGsmDispatcher.sendText(destAddr, scAddr,
- text, sentIntent, deliveryIntent, messageUri, callingPkg, persistMessage,
- priority, expectMore, validityPeriod);
+ text, sentIntent, deliveryIntent, messageUri, callingPkg, persistMessage);
}
}
@@ -369,7 +365,7 @@ public class ImsSMSDispatcher extends SMSDispatcher {
String message, SmsHeader smsHeader, int format, PendingIntent sentIntent,
PendingIntent deliveryIntent, boolean lastPart,
AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
- String fullMessageText, int priority, boolean expectMore, int validityPeriod) {
+ String fullMessageText) {
Rlog.e(TAG, "Error! Not implemented for IMS.");
return null;
}
diff --git a/com/android/internal/telephony/MccTable.java b/com/android/internal/telephony/MccTable.java
index 3220078b..a0106182 100644
--- a/com/android/internal/telephony/MccTable.java
+++ b/com/android/internal/telephony/MccTable.java
@@ -17,7 +17,6 @@
package com.android.internal.telephony;
import android.app.ActivityManager;
-import android.app.AlarmManager;
import android.content.Context;
import android.content.res.Configuration;
import android.net.wifi.WifiManager;
@@ -359,21 +358,11 @@ public final class MccTable {
* @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA)
*/
private static void setTimezoneFromMccIfNeeded(Context context, int mcc) {
- String timezone = SystemProperties.get(ServiceStateTracker.TIMEZONE_PROPERTY);
- // timezone.equals("GMT") will be true and only true if the timezone was
- // set to a default value by the system server (when starting, system server.
- // sets the persist.sys.timezone to "GMT" if it's not set)."GMT" is not used by
- // any code that sets it explicitly (in case where something sets GMT explicitly,
- // "Etc/GMT" Olsen ID would be used).
- // TODO(b/64056758): Remove "timezone.equals("GMT")" hack when there's a
- // better way of telling if the value has been defaulted.
- if (timezone == null || timezone.length() == 0 || timezone.equals("GMT")) {
+ if (!TimeServiceHelper.isTimeZoneSettingInitializedStatic()) {
String zoneId = defaultTimeZoneForMcc(mcc);
if (zoneId != null && zoneId.length() > 0) {
// Set time zone based on MCC
- AlarmManager alarm =
- (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- alarm.setTimeZone(zoneId);
+ TimeServiceHelper.setDeviceTimeZoneStatic(context, zoneId);
Slog.d(LOG_TAG, "timezone set to " + zoneId);
}
}
diff --git a/com/android/internal/telephony/NetworkScanRequestTracker.java b/com/android/internal/telephony/NetworkScanRequestTracker.java
index 46b1eef9..c6198d1f 100644
--- a/com/android/internal/telephony/NetworkScanRequestTracker.java
+++ b/com/android/internal/telephony/NetworkScanRequestTracker.java
@@ -107,50 +107,52 @@ public final class NetworkScanRequestTracker {
}
private boolean isValidScan(NetworkScanRequestInfo nsri) {
- if (nsri.mRequest.specifiers == null) {
+ if (nsri.mRequest == null || nsri.mRequest.getSpecifiers() == null) {
return false;
}
- if (nsri.mRequest.specifiers.length > NetworkScanRequest.MAX_RADIO_ACCESS_NETWORKS) {
+ if (nsri.mRequest.getSpecifiers().length > NetworkScanRequest.MAX_RADIO_ACCESS_NETWORKS) {
return false;
}
- for (RadioAccessSpecifier ras : nsri.mRequest.specifiers) {
- if (ras.radioAccessNetwork != GERAN && ras.radioAccessNetwork != UTRAN
- && ras.radioAccessNetwork != EUTRAN) {
+ for (RadioAccessSpecifier ras : nsri.mRequest.getSpecifiers()) {
+ if (ras.getRadioAccessNetwork() != GERAN && ras.getRadioAccessNetwork() != UTRAN
+ && ras.getRadioAccessNetwork() != EUTRAN) {
return false;
}
- if (ras.bands != null && ras.bands.length > NetworkScanRequest.MAX_BANDS) {
+ if (ras.getBands() != null && ras.getBands().length > NetworkScanRequest.MAX_BANDS) {
return false;
}
- if (ras.channels != null && ras.channels.length > NetworkScanRequest.MAX_CHANNELS) {
+ if (ras.getChannels() != null
+ && ras.getChannels().length > NetworkScanRequest.MAX_CHANNELS) {
return false;
}
}
- if ((nsri.mRequest.searchPeriodicity < NetworkScanRequest.MIN_SEARCH_PERIODICITY_SEC)
- || (nsri.mRequest.searchPeriodicity
+ if ((nsri.mRequest.getSearchPeriodicity() < NetworkScanRequest.MIN_SEARCH_PERIODICITY_SEC)
+ || (nsri.mRequest.getSearchPeriodicity()
> NetworkScanRequest.MAX_SEARCH_PERIODICITY_SEC)) {
return false;
}
- if ((nsri.mRequest.maxSearchTime < NetworkScanRequest.MIN_SEARCH_MAX_SEC)
- || (nsri.mRequest.maxSearchTime > NetworkScanRequest.MAX_SEARCH_MAX_SEC)) {
+ if ((nsri.mRequest.getMaxSearchTime() < NetworkScanRequest.MIN_SEARCH_MAX_SEC)
+ || (nsri.mRequest.getMaxSearchTime() > NetworkScanRequest.MAX_SEARCH_MAX_SEC)) {
return false;
}
- if ((nsri.mRequest.incrementalResultsPeriodicity
+ if ((nsri.mRequest.getIncrementalResultsPeriodicity()
< NetworkScanRequest.MIN_INCREMENTAL_PERIODICITY_SEC)
- || (nsri.mRequest.incrementalResultsPeriodicity
+ || (nsri.mRequest.getIncrementalResultsPeriodicity()
> NetworkScanRequest.MAX_INCREMENTAL_PERIODICITY_SEC)) {
return false;
}
- if ((nsri.mRequest.searchPeriodicity > nsri.mRequest.maxSearchTime)
- || (nsri.mRequest.incrementalResultsPeriodicity > nsri.mRequest.maxSearchTime)) {
+ if ((nsri.mRequest.getSearchPeriodicity() > nsri.mRequest.getMaxSearchTime())
+ || (nsri.mRequest.getIncrementalResultsPeriodicity()
+ > nsri.mRequest.getMaxSearchTime())) {
return false;
}
- if ((nsri.mRequest.mccMncs != null)
- && (nsri.mRequest.mccMncs.size() > NetworkScanRequest.MAX_MCC_MNC_LIST_SIZE)) {
+ if ((nsri.mRequest.getPlmns() != null)
+ && (nsri.mRequest.getPlmns().size() > NetworkScanRequest.MAX_MCC_MNC_LIST_SIZE)) {
return false;
}
return true;
@@ -274,10 +276,10 @@ public final class NetworkScanRequestTracker {
return NetworkScan.ERROR_INVALID_SCAN;
case RadioError.DEVICE_IN_USE:
Log.e(TAG, "rilErrorToScanError: DEVICE_IN_USE");
- return NetworkScan.ERROR_MODEM_BUSY;
+ return NetworkScan.ERROR_MODEM_UNAVAILABLE;
default:
Log.e(TAG, "rilErrorToScanError: Unexpected RadioError " + rilError);
- return NetworkScan.ERROR_RIL_ERROR;
+ return NetworkScan.ERROR_RADIO_INTERFACE_ERROR;
}
}
@@ -306,11 +308,11 @@ public final class NetworkScanRequestTracker {
return NetworkScan.ERROR_INVALID_SCAN;
case DEVICE_IN_USE:
Log.e(TAG, "commandExceptionErrorToScanError: DEVICE_IN_USE");
- return NetworkScan.ERROR_MODEM_BUSY;
+ return NetworkScan.ERROR_MODEM_UNAVAILABLE;
default:
Log.e(TAG, "commandExceptionErrorToScanError: Unexpected CommandExceptionError "
+ error);
- return NetworkScan.ERROR_RIL_ERROR;
+ return NetworkScan.ERROR_RADIO_INTERFACE_ERROR;
}
}
@@ -332,7 +334,7 @@ public final class NetworkScanRequestTracker {
if (!interruptLiveScan(nsri)) {
if (!cacheScan(nsri)) {
notifyMessenger(nsri, TelephonyScanManager.CALLBACK_SCAN_ERROR,
- NetworkScan.ERROR_MODEM_BUSY, null);
+ NetworkScan.ERROR_MODEM_UNAVAILABLE, null);
}
}
}
@@ -389,7 +391,7 @@ public final class NetworkScanRequestTracker {
}
} else {
logEmptyResultOrException(ar);
- deleteScanAndMayNotify(nsri, NetworkScan.ERROR_RIL_ERROR, true);
+ deleteScanAndMayNotify(nsri, NetworkScan.ERROR_RADIO_INTERFACE_ERROR, true);
nsri.mPhone.mCi.unregisterForNetworkScanResult(mHandler);
}
}
diff --git a/com/android/internal/telephony/NitzData.java b/com/android/internal/telephony/NitzData.java
index 6775639d..df3541b4 100644
--- a/com/android/internal/telephony/NitzData.java
+++ b/com/android/internal/telephony/NitzData.java
@@ -54,6 +54,9 @@ public final class NitzData {
private NitzData(String originalString, int zoneOffsetMillis, Integer dstOffsetMillis,
long utcTimeMillis, TimeZone timeZone) {
+ if (originalString == null) {
+ throw new NullPointerException("originalString==null");
+ }
this.mOriginalString = originalString;
this.mZoneOffset = zoneOffsetMillis;
this.mDstOffset = dstOffsetMillis;
@@ -134,6 +137,13 @@ public final class NitzData {
}
}
+ /** A method for use in tests to create NitzData instances. */
+ public static NitzData createForTests(int zoneOffsetMillis, Integer dstOffsetMillis,
+ long utcTimeMillis, TimeZone timeZone) {
+ return new NitzData("Test data", zoneOffsetMillis, dstOffsetMillis, utcTimeMillis,
+ timeZone);
+ }
+
/**
* Returns the current time as the number of milliseconds since the beginning of the Unix epoch
* (1/1/1970 00:00:00 UTC).
@@ -216,6 +226,45 @@ public final class NitzData {
}
@Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ NitzData nitzData = (NitzData) o;
+
+ if (mZoneOffset != nitzData.mZoneOffset) {
+ return false;
+ }
+ if (mCurrentTimeMillis != nitzData.mCurrentTimeMillis) {
+ return false;
+ }
+ if (!mOriginalString.equals(nitzData.mOriginalString)) {
+ return false;
+ }
+ if (mDstOffset != null ? !mDstOffset.equals(nitzData.mDstOffset)
+ : nitzData.mDstOffset != null) {
+ return false;
+ }
+ return mEmulatorHostTimeZone != null ? mEmulatorHostTimeZone
+ .equals(nitzData.mEmulatorHostTimeZone) : nitzData.mEmulatorHostTimeZone == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mOriginalString.hashCode();
+ result = 31 * result + mZoneOffset;
+ result = 31 * result + (mDstOffset != null ? mDstOffset.hashCode() : 0);
+ result = 31 * result + (int) (mCurrentTimeMillis ^ (mCurrentTimeMillis >>> 32));
+ result = 31 * result + (mEmulatorHostTimeZone != null ? mEmulatorHostTimeZone.hashCode()
+ : 0);
+ return result;
+ }
+
+ @Override
public String toString() {
return "NitzData{"
+ "mOriginalString=" + mOriginalString
diff --git a/com/android/internal/telephony/NitzStateMachine.java b/com/android/internal/telephony/NitzStateMachine.java
new file mode 100644
index 00000000..b1aa1f0d
--- /dev/null
+++ b/com/android/internal/telephony/NitzStateMachine.java
@@ -0,0 +1,652 @@
+/*
+ * 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 com.android.internal.telephony;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.TimeZone;
+
+/**
+ * {@hide}
+ */
+// Non-final to allow mocking.
+public class NitzStateMachine {
+
+ /**
+ * A proxy over device state that allows things like system properties, system clock
+ * to be faked for tests.
+ */
+ // Non-final to allow mocking.
+ public static class DeviceState {
+ private static final int NITZ_UPDATE_SPACING_DEFAULT = 1000 * 60 * 10;
+ private final int mNitzUpdateSpacing;
+
+ private static final int NITZ_UPDATE_DIFF_DEFAULT = 2000;
+ private final int mNitzUpdateDiff;
+
+ private final GsmCdmaPhone mPhone;
+ private final TelephonyManager mTelephonyManager;
+ private final ContentResolver mCr;
+
+ public DeviceState(GsmCdmaPhone phone) {
+ mPhone = phone;
+
+ Context context = phone.getContext();
+ mTelephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ mCr = context.getContentResolver();
+ mNitzUpdateSpacing =
+ SystemProperties.getInt("ro.nitz_update_spacing", NITZ_UPDATE_SPACING_DEFAULT);
+ mNitzUpdateDiff =
+ SystemProperties.getInt("ro.nitz_update_diff", NITZ_UPDATE_DIFF_DEFAULT);
+ }
+
+ /**
+ * If time between NITZ updates is less than {@link #getNitzUpdateSpacingMillis()} the
+ * update may be ignored.
+ */
+ public int getNitzUpdateSpacingMillis() {
+ return Settings.Global.getInt(mCr, Settings.Global.NITZ_UPDATE_SPACING,
+ mNitzUpdateSpacing);
+ }
+
+ /**
+ * If {@link #getNitzUpdateSpacingMillis()} hasn't been exceeded but update is >
+ * {@link #getNitzUpdateDiffMillis()} do the update
+ */
+ public int getNitzUpdateDiffMillis() {
+ return Settings.Global.getInt(mCr, Settings.Global.NITZ_UPDATE_DIFF, mNitzUpdateDiff);
+ }
+
+ /**
+ * Returns true if the {@code gsm.ignore-nitz} system property is set to "yes".
+ */
+ public boolean getIgnoreNitz() {
+ String ignoreNitz = SystemProperties.get("gsm.ignore-nitz");
+ return ignoreNitz != null && ignoreNitz.equals("yes");
+ }
+
+ /**
+ * Returns the same value as {@link SystemClock#elapsedRealtime()}.
+ */
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ public String getNetworkCountryIsoForPhone() {
+ return mTelephonyManager.getNetworkCountryIsoForPhone(mPhone.getPhoneId());
+ }
+ }
+
+ private static final String LOG_TAG = ServiceStateTracker.LOG_TAG;
+ private static final boolean DBG = ServiceStateTracker.DBG;
+
+ /**
+ * List of ISO codes for countries that can have an offset of
+ * GMT+0 when not in daylight savings time. This ignores some
+ * small places such as the Canary Islands (Spain) and
+ * Danmarkshavn (Denmark). The list must be sorted by code.
+ */
+ private static final String[] GMT_COUNTRY_CODES = {
+ "bf", // Burkina Faso
+ "ci", // Cote d'Ivoire
+ "eh", // Western Sahara
+ "fo", // Faroe Islands, Denmark
+ "gb", // United Kingdom of Great Britain and Northern Ireland
+ "gh", // Ghana
+ "gm", // Gambia
+ "gn", // Guinea
+ "gw", // Guinea Bissau
+ "ie", // Ireland
+ "lr", // Liberia
+ "is", // Iceland
+ "ma", // Morocco
+ "ml", // Mali
+ "mr", // Mauritania
+ "pt", // Portugal
+ "sl", // Sierra Leone
+ "sn", // Senegal
+ "st", // Sao Tome and Principe
+ "tg", // Togo
+ };
+
+ private final LocalLog mTimeLog = new LocalLog(15);
+ private final LocalLog mTimeZoneLog = new LocalLog(15);
+
+ /**
+ * Sometimes we get the NITZ time before we know what country we
+ * are in. Keep the time zone information from the NITZ string in
+ * mNitzData so we can fix the time zone once know the country.
+ */
+ private boolean mNeedFixZoneAfterNitz = false;
+
+ private NitzData mNitzData;
+ private boolean mGotCountryCode = false;
+ private String mSavedTimeZoneId;
+ private long mSavedTime;
+ private long mSavedAtTime;
+
+ /** Wake lock used while setting time of day. */
+ private PowerManager.WakeLock mWakeLock;
+ private static final String WAKELOCK_TAG = "NitzStateMachine";
+
+ /** Boolean is true if setTimeFromNITZ was called */
+ private boolean mNitzUpdatedTime = false;
+
+ private final GsmCdmaPhone mPhone;
+ private final DeviceState mDeviceState;
+ private final TimeServiceHelper mTimeServiceHelper;
+
+ public NitzStateMachine(GsmCdmaPhone phone) {
+ this(phone,
+ TelephonyComponentFactory.getInstance().makeTimeServiceHelper(phone.getContext()),
+ new DeviceState(phone));
+ }
+
+ @VisibleForTesting
+ public NitzStateMachine(GsmCdmaPhone phone, TimeServiceHelper timeServiceHelper,
+ DeviceState deviceState) {
+ mPhone = phone;
+
+ Context context = phone.getContext();
+ PowerManager powerManager =
+ (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
+
+ mDeviceState = deviceState;
+ mTimeServiceHelper = timeServiceHelper;
+ mTimeServiceHelper.setListener(new TimeServiceHelper.Listener() {
+ @Override
+ public void onTimeDetectionChange(boolean enabled) {
+ if (enabled) {
+ revertToNitzTime();
+ }
+ }
+
+ @Override
+ public void onTimeZoneDetectionChange(boolean enabled) {
+ if (enabled) {
+ revertToNitzTimeZone();
+ }
+ }
+ });
+ }
+
+ /**
+ * Called when the device's network country is known, allowing the time zone detection to be
+ * substantially more precise.
+ */
+ public void fixTimeZone(String isoCountryCode) {
+ // Capture the time zone property. This allows us to tell whether the device has a time zone
+ // set. TimeZone.getDefault() returns a default zone (GMT) even when time zone is not
+ // explicitly set making the system property a better indicator.
+ final boolean isTimeZoneSettingInitialized =
+ mTimeServiceHelper.isTimeZoneSettingInitialized();
+ if (DBG) {
+ Rlog.d(LOG_TAG, "fixTimeZone"
+ + " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
+ + " mNitzData=" + mNitzData
+ + " iso-cc='" + isoCountryCode
+ + "' iso-cc-idx=" + Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode));
+ }
+ TimeZone zone;
+ if ("".equals(isoCountryCode) && mNeedFixZoneAfterNitz) {
+ // Country code not found. This is likely a test network.
+ // Get a TimeZone based only on the NITZ parameters (best guess).
+
+ // mNeedFixZoneAfterNitz is only set to true when mNitzData is set so there's no need to
+ // check mNitzData == null.
+ zone = NitzData.guessTimeZone(mNitzData);
+ if (DBG) {
+ Rlog.d(LOG_TAG, "fixTimeZone(): guessNitzTimeZone returned "
+ + (zone == null ? zone : zone.getID()));
+ }
+ } else if ((mNitzData == null || nitzOffsetMightBeBogus(mNitzData))
+ && isTimeZoneSettingInitialized
+ && (Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode) < 0)) {
+
+ // This case means that (1) the device received no NITZ signal yet or received an NITZ
+ // signal that looks bogus due to having a zero offset from UTC, (2) the device has a
+ // time zone set explicitly, and (3) the iso tells us the country is NOT one that uses a
+ // zero offset. This is interpreted as being NITZ incorrectly reporting a local time and
+ // not a UTC time. The zone is left as the current device's zone setting, and the time
+ // may be adjusted by assuming the current zone setting is correct.
+ zone = TimeZone.getDefault();
+
+ // Note that mNeedFixZoneAfterNitz => (implies) { mNitzData != null }. Therefore, if
+ // mNitzData == null, mNeedFixZoneAfterNitz cannot be true. The code in this section
+ // therefore means that when mNitzData == null (and the country is one that doesn't use
+ // a zero UTC offset) the device will retain the existing time zone setting and not try
+ // to derive one from the isoCountryCode.
+ if (mNeedFixZoneAfterNitz) {
+ long ctm = System.currentTimeMillis();
+ long tzOffset = zone.getOffset(ctm);
+ if (DBG) {
+ Rlog.d(LOG_TAG, "fixTimeZone: tzOffset=" + tzOffset
+ + " ltod=" + TimeUtils.logTimeOfDay(ctm));
+ }
+ if (mTimeServiceHelper.isTimeDetectionEnabled()) {
+ long adj = ctm - tzOffset;
+ if (DBG) {
+ Rlog.d(LOG_TAG, "fixTimeZone: adj ltod=" + TimeUtils.logTimeOfDay(adj));
+ }
+ setAndBroadcastNetworkSetTime(adj);
+ } else {
+ // Adjust the saved NITZ time to account for tzOffset.
+ mSavedTime = mSavedTime - tzOffset;
+ if (DBG) {
+ Rlog.d(LOG_TAG, "fixTimeZone: adj mSavedTime=" + mSavedTime);
+ }
+ }
+ }
+ if (DBG) {
+ Rlog.d(LOG_TAG, "fixTimeZone: using default TimeZone");
+ }
+ } else if (mNitzData == null) {
+ // The use of 1/1/1970 UTC is unusual but consistent with historical behavior when
+ // it wasn't possible to detect whether a previous NITZ signal had been saved.
+ zone = TimeUtils.getTimeZone(0 /* offset */, false /* dst */, 0 /* when */,
+ isoCountryCode);
+ if (DBG) {
+ Rlog.d(LOG_TAG, "fixTimeZone: No cached NITZ data available, using only country"
+ + " code. zone=" + zone);
+ }
+ } else {
+ zone = TimeUtils.getTimeZone(mNitzData.getLocalOffsetMillis(), mNitzData.isDst(),
+ mNitzData.getCurrentTimeInMillis(), isoCountryCode);
+ if (DBG) {
+ Rlog.d(LOG_TAG, "fixTimeZone: using getTimeZone(off, dst, time, iso)");
+ }
+ }
+ final String tmpLog = "fixTimeZone:"
+ + " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
+ + " mNitzData=" + mNitzData
+ + " iso-cc=" + isoCountryCode
+ + " mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz
+ + " zone=" + (zone != null ? zone.getID() : "NULL");
+ mTimeZoneLog.log(tmpLog);
+
+ if (zone != null) {
+ Rlog.d(LOG_TAG, "fixTimeZone: zone != null zone.getID=" + zone.getID());
+ if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
+ setAndBroadcastNetworkSetTimeZone(zone.getID());
+ } else {
+ Rlog.d(LOG_TAG, "fixTimeZone: skip changing zone as getAutoTimeZone was false");
+ }
+ if (mNeedFixZoneAfterNitz) {
+ saveNitzTimeZone(zone.getID());
+ }
+ } else {
+ Rlog.d(LOG_TAG, "fixTimeZone: zone == null, do nothing for zone");
+ }
+ mNeedFixZoneAfterNitz = false;
+ }
+
+ /**
+ * Returns {@code true} if the NITZ data looks like it might be incomplete or bogus, i.e. it has
+ * a zero offset from UTC with either no DST information available or a zero DST offset.
+ */
+ private static boolean nitzOffsetMightBeBogus(NitzData nitzData) {
+ return nitzData.getLocalOffsetMillis() == 0 && !nitzData.isDst();
+ }
+
+ /**
+ * Handle a new NITZ signal being received.
+ */
+ public void setTimeAndTimeZoneFromNitz(NitzData newNitzData, long nitzReceiveTime) {
+ setTimeZoneFromNitz(newNitzData, nitzReceiveTime);
+ setTimeFromNitz(newNitzData, nitzReceiveTime);
+ }
+
+ private void setTimeZoneFromNitz(NitzData newNitzData, long nitzReceiveTime) {
+ try {
+ String iso = mDeviceState.getNetworkCountryIsoForPhone();
+ TimeZone zone;
+ if (newNitzData.getEmulatorHostTimeZone() != null) {
+ zone = newNitzData.getEmulatorHostTimeZone();
+ } else {
+ if (!mGotCountryCode) {
+ zone = null;
+ } else if (iso != null && iso.length() > 0) {
+ zone = TimeUtils.getTimeZone(
+ newNitzData.getLocalOffsetMillis(),
+ newNitzData.isDst(),
+ newNitzData.getCurrentTimeInMillis(),
+ iso);
+ } else {
+ // We don't have a valid iso country code. This is
+ // most likely because we're on a test network that's
+ // using a bogus MCC (eg, "001"), so get a TimeZone
+ // based only on the NITZ parameters.
+ zone = NitzData.guessTimeZone(newNitzData);
+ if (DBG) {
+ Rlog.d(LOG_TAG, "setTimeFromNITZ(): guessNitzTimeZone returned "
+ + (zone == null ? zone : zone.getID()));
+ }
+ }
+ }
+
+ int previousUtcOffset;
+ boolean previousIsDst;
+ if (mNitzData == null) {
+ // No previously saved NITZ data. Use the same defaults as Android would have done
+ // before it was possible to detect this case.
+ previousUtcOffset = 0;
+ previousIsDst = false;
+ } else {
+ previousUtcOffset = mNitzData.getLocalOffsetMillis();
+ previousIsDst = mNitzData.isDst();
+ }
+ if ((zone == null)
+ || (newNitzData.getLocalOffsetMillis() != previousUtcOffset)
+ || (newNitzData.isDst() != previousIsDst)) {
+ // We got the time before the country or the zone has changed
+ // so we don't know how to identify the DST rules yet. Save
+ // the information and hope to fix it up later.
+ mNeedFixZoneAfterNitz = true;
+ mNitzData = newNitzData;
+ }
+
+ String tmpLog = "NITZ: newNitzData=" + newNitzData
+ + " nitzReceiveTime=" + nitzReceiveTime
+ + " zone=" + (zone != null ? zone.getID() : "NULL")
+ + " iso=" + iso + " mGotCountryCode=" + mGotCountryCode
+ + " mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz
+ + " isTimeZoneDetectionEnabled()="
+ + mTimeServiceHelper.isTimeZoneDetectionEnabled();
+ if (DBG) {
+ Rlog.d(LOG_TAG, tmpLog);
+ }
+ mTimeZoneLog.log(tmpLog);
+
+ if (zone != null) {
+ if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
+ setAndBroadcastNetworkSetTimeZone(zone.getID());
+ }
+ saveNitzTimeZone(zone.getID());
+ }
+ } catch (RuntimeException ex) {
+ Rlog.e(LOG_TAG, "NITZ: Processing NITZ data " + newNitzData + " ex=" + ex);
+ }
+ }
+
+ private void setTimeFromNitz(NitzData newNitzData, long nitzReceiveTime) {
+ try {
+ boolean ignoreNitz = mDeviceState.getIgnoreNitz();
+ if (ignoreNitz) {
+ Rlog.d(LOG_TAG, "NITZ: Not setting clock because gsm.ignore-nitz is set");
+ return;
+ }
+
+ try {
+ mWakeLock.acquire();
+
+ long millisSinceNitzReceived = mDeviceState.elapsedRealtime() - nitzReceiveTime;
+ if (millisSinceNitzReceived < 0) {
+ // Sanity check: something is wrong
+ if (DBG) {
+ Rlog.d(LOG_TAG, "NITZ: not setting time, clock has rolled "
+ + "backwards since NITZ time was received, "
+ + newNitzData);
+ }
+ return;
+ }
+
+ if (millisSinceNitzReceived > Integer.MAX_VALUE) {
+ // If the time is this far off, something is wrong > 24 days!
+ if (DBG) {
+ Rlog.d(LOG_TAG, "NITZ: not setting time, processing has taken "
+ + (millisSinceNitzReceived / (1000 * 60 * 60 * 24))
+ + " days");
+ }
+ return;
+ }
+
+ // Adjust the NITZ time by the delay since it was received.
+ long adjustedCurrentTimeMillis = newNitzData.getCurrentTimeInMillis();
+ adjustedCurrentTimeMillis += millisSinceNitzReceived;
+
+ if (mTimeServiceHelper.isTimeDetectionEnabled()) {
+ String tmpLog = "NITZ: newNitaData=" + newNitzData
+ + " nitzReceiveTime=" + nitzReceiveTime
+ + " Setting time of day to " + adjustedCurrentTimeMillis
+ + " NITZ receive delay(ms): " + millisSinceNitzReceived
+ + " gained(ms): "
+ + (adjustedCurrentTimeMillis - System.currentTimeMillis());
+ if (DBG) {
+ Rlog.d(LOG_TAG, tmpLog);
+ }
+ mTimeLog.log(tmpLog);
+ // Update system time automatically
+ long gained = adjustedCurrentTimeMillis - System.currentTimeMillis();
+ long timeSinceLastUpdate =
+ mDeviceState.elapsedRealtime() - mSavedAtTime;
+ int nitzUpdateSpacing = mDeviceState.getNitzUpdateSpacingMillis();
+ int nitzUpdateDiff = mDeviceState.getNitzUpdateDiffMillis();
+ if ((mSavedAtTime == 0) || (timeSinceLastUpdate > nitzUpdateSpacing)
+ || (Math.abs(gained) > nitzUpdateDiff)) {
+ if (DBG) {
+ Rlog.d(LOG_TAG, "NITZ: Auto updating time of day to "
+ + adjustedCurrentTimeMillis
+ + " NITZ receive delay=" + millisSinceNitzReceived
+ + "ms gained=" + gained + "ms from " + newNitzData);
+ }
+
+ setAndBroadcastNetworkSetTime(adjustedCurrentTimeMillis);
+ } else {
+ if (DBG) {
+ Rlog.d(LOG_TAG, "NITZ: ignore, a previous update was "
+ + timeSinceLastUpdate + "ms ago and gained="
+ + gained + "ms");
+ }
+ return;
+ }
+ }
+ saveNitzTime(adjustedCurrentTimeMillis);
+ } finally {
+ mWakeLock.release();
+ }
+ } catch (RuntimeException ex) {
+ Rlog.e(LOG_TAG, "NITZ: Processing NITZ data " + newNitzData + " ex=" + ex);
+ }
+ }
+
+ private void saveNitzTimeZone(String zoneId) {
+ mSavedTimeZoneId = zoneId;
+ }
+
+ private void saveNitzTime(long time) {
+ mSavedTime = time;
+ mSavedAtTime = mDeviceState.elapsedRealtime();
+ mNitzUpdatedTime = true;
+ }
+
+ private void setAndBroadcastNetworkSetTimeZone(String zoneId) {
+ if (DBG) {
+ Rlog.d(LOG_TAG, "setAndBroadcastNetworkSetTimeZone: setTimeZone=" + zoneId);
+ }
+ mTimeServiceHelper.setDeviceTimeZone(zoneId);
+ if (DBG) {
+ Rlog.d(LOG_TAG,
+ "setAndBroadcastNetworkSetTimeZone: call alarm.setTimeZone and broadcast"
+ + " zoneId=" + zoneId);
+ }
+ }
+
+ private void setAndBroadcastNetworkSetTime(long time) {
+ if (DBG) {
+ Rlog.d(LOG_TAG, "setAndBroadcastNetworkSetTime: time=" + time + "ms");
+ }
+ mTimeServiceHelper.setDeviceTime(time);
+ TelephonyMetrics.getInstance().writeNITZEvent(mPhone.getPhoneId(), time);
+ }
+
+ private void revertToNitzTime() {
+ if (DBG) {
+ Rlog.d(LOG_TAG, "Reverting to NITZ Time: mSavedTime=" + mSavedTime
+ + " mSavedAtTime=" + mSavedAtTime);
+ }
+ if (mSavedTime != 0 && mSavedAtTime != 0) {
+ long currTime = mDeviceState.elapsedRealtime();
+ mTimeLog.log("Reverting to NITZ time, currTime=" + currTime
+ + " mSavedAtTime=" + mSavedAtTime + " mSavedTime=" + mSavedTime);
+ setAndBroadcastNetworkSetTime(mSavedTime + (currTime - mSavedAtTime));
+ }
+ }
+
+ private void revertToNitzTimeZone() {
+ String tmpLog = "Reverting to NITZ TimeZone: tz=" + mSavedTimeZoneId;
+ if (DBG) {
+ Rlog.d(LOG_TAG, tmpLog);
+ }
+ mTimeZoneLog.log(tmpLog);
+ if (mSavedTimeZoneId != null) {
+ setAndBroadcastNetworkSetTimeZone(mSavedTimeZoneId);
+ } else {
+ String iso = mDeviceState.getNetworkCountryIsoForPhone();
+ if (!TextUtils.isEmpty(iso)) {
+ updateTimeZoneByNetworkCountryCode(iso);
+ }
+ }
+ }
+
+ /**
+ * Dumps the current in-memory state to the supplied PrintWriter.
+ */
+ public void dumpState(PrintWriter pw) {
+ pw.println(" mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz);
+ pw.println(" mNitzData=" + mNitzData);
+ pw.println(" mGotCountryCode=" + mGotCountryCode);
+ pw.println(" mSavedTimeZone=" + mSavedTimeZoneId);
+ pw.println(" mSavedTime=" + mSavedTime);
+ pw.println(" mSavedAtTime=" + mSavedAtTime);
+ pw.println(" mWakeLock=" + mWakeLock);
+ pw.println(" mNitzUpdatedTime=" + mNitzUpdatedTime);
+ pw.flush();
+ }
+
+ /**
+ * Dumps the time / time zone logs to the supplied IndentingPrintWriter.
+ */
+ public void dumpLogs(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
+ ipw.println(" Time Logs:");
+ ipw.increaseIndent();
+ mTimeLog.dump(fd, ipw, args);
+ ipw.decreaseIndent();
+
+ ipw.println(" Time zone Logs:");
+ ipw.increaseIndent();
+ mTimeZoneLog.dump(fd, ipw, args);
+ ipw.decreaseIndent();
+ }
+
+ /**
+ * Update time zone by network country code, works on countries which only have one time zone.
+ *
+ * @param iso Country code from network MCC
+ */
+ public void updateTimeZoneByNetworkCountryCode(String iso) {
+ List<String> uniqueZoneIds = TimeUtils.getTimeZoneIdsWithUniqueOffsets(iso);
+ if (uniqueZoneIds.size() == 1) {
+ String zoneId = uniqueZoneIds.get(0);
+ if (DBG) {
+ Rlog.d(LOG_TAG, "updateTimeZoneByNetworkCountryCode: no nitz but one TZ for iso-cc="
+ + iso
+ + " with zone.getID=" + zoneId);
+ }
+ mTimeZoneLog.log("updateTimeZoneByNetworkCountryCode: set time zone=" + zoneId
+ + " iso=" + iso);
+ setAndBroadcastNetworkSetTimeZone(zoneId);
+ } else {
+ if (DBG) {
+ Rlog.d(LOG_TAG,
+ "updateTimeZoneByNetworkCountryCode: there are " + uniqueZoneIds.size()
+ + " unique offsets for iso-cc='" + iso
+ + "', do nothing");
+ }
+ }
+ }
+
+ /**
+ * Clear the mNitzUpdatedTime flag.
+ */
+ public void clearNitzUpdatedTime() {
+ mNitzUpdatedTime = false;
+ }
+
+ /**
+ * Get the mNitzUpdatedTime flag value.
+ */
+ public boolean getNitzUpdatedTime() {
+ return mNitzUpdatedTime;
+ }
+
+ /**
+ * Sets the mGotCountryCode flag to the specified value.
+ */
+ public void setNetworkCountryIsoAvailable(boolean gotCountryCode) {
+ mGotCountryCode = gotCountryCode;
+ }
+
+ /**
+ * Returns true if mNitzUpdatedTime and automatic time zone detection is enabled.
+ */
+ public boolean shouldUpdateTimeZoneUsingCountryCode() {
+ return !mNitzUpdatedTime && mTimeServiceHelper.isTimeZoneDetectionEnabled();
+ }
+
+ /**
+ * Returns the last NITZ data that was cached.
+ */
+ public NitzData getCachedNitzData() {
+ return mNitzData;
+ }
+
+ /**
+ * Returns the time zone ID from the most recent time that a time zone could be determined by
+ * this state machine.
+ */
+ public String getSavedTimeZoneId() {
+ return mSavedTimeZoneId;
+ }
+
+ /**
+ * Returns the mNeedFixZoneAfterNitz flag value.
+ */
+ public boolean fixTimeZoneCallNeeded() {
+ return mNeedFixZoneAfterNitz;
+ }
+
+}
diff --git a/com/android/internal/telephony/Phone.java b/com/android/internal/telephony/Phone.java
index d702c09b..a2aeb441 100644
--- a/com/android/internal/telephony/Phone.java
+++ b/com/android/internal/telephony/Phone.java
@@ -54,6 +54,7 @@ import android.telephony.Rlog;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
import android.telephony.VoLteServiceState;
import android.text.TextUtils;
@@ -120,14 +121,10 @@ public abstract class Phone extends Handler implements PhoneInternalInterface {
if (intent.getAction().equals(ImsManager.ACTION_IMS_SERVICE_UP)) {
mImsServiceReady = true;
updateImsPhone();
- ImsManager.updateImsServiceConfig(mContext, mPhoneId, false);
+ ImsManager.getInstance(mContext, mPhoneId).updateImsServiceConfig(false);
} else if (intent.getAction().equals(ImsManager.ACTION_IMS_SERVICE_DOWN)) {
mImsServiceReady = false;
updateImsPhone();
- } else if (intent.getAction().equals(ImsConfig.ACTION_IMS_CONFIG_CHANGED)) {
- int item = intent.getIntExtra(ImsConfig.EXTRA_CHANGED_ITEM, -1);
- String value = intent.getStringExtra(ImsConfig.EXTRA_NEW_VALUE);
- ImsManager.onProvisionedValueChanged(context, item, value);
}
}
}
@@ -561,7 +558,6 @@ public abstract class Phone extends Handler implements PhoneInternalInterface {
filter.addAction(ImsManager.ACTION_IMS_SERVICE_UP);
filter.addAction(ImsManager.ACTION_IMS_SERVICE_DOWN);
}
- filter.addAction(ImsConfig.ACTION_IMS_CONFIG_CHANGED);
mContext.registerReceiver(mImsIntentReceiver, filter);
// Monitor IMS service - but first poll to see if already up (could miss
@@ -2993,8 +2989,7 @@ public abstract class Phone extends Handler implements PhoneInternalInterface {
}
public int getCarrierId() {
- // TODO remove hardcoding and expose a public API for INVALID CARRIER ID
- return -1;
+ return TelephonyManager.UNKNOWN_CARRIER_ID;
}
public String getCarrierName() {
@@ -3332,12 +3327,11 @@ public abstract class Phone extends Handler implements PhoneInternalInterface {
* @return {@code true} if IMS calling is enabled.
*/
public boolean isImsUseEnabled() {
- boolean imsUseEnabled =
- ((ImsManager.isVolteEnabledByPlatform(mContext) &&
- ImsManager.isEnhanced4gLteModeSettingEnabledByUser(mContext)) ||
- (ImsManager.isWfcEnabledByPlatform(mContext) &&
- ImsManager.isWfcEnabledByUser(mContext)) &&
- ImsManager.isNonTtyOrTtyOnVolteEnabled(mContext));
+ ImsManager imsManager = ImsManager.getInstance(mContext, mPhoneId);
+ boolean imsUseEnabled = ((imsManager.isVolteEnabledByPlatform()
+ && imsManager.isEnhanced4gLteModeSettingEnabledByUser())
+ || (imsManager.isWfcEnabledByPlatform() && imsManager.isWfcEnabledByUser())
+ && imsManager.isNonTtyOrTtyOnVolteEnabled());
return imsUseEnabled;
}
@@ -3458,13 +3452,13 @@ public abstract class Phone extends Handler implements PhoneInternalInterface {
return false;
}
- public static void checkWfcWifiOnlyModeBeforeDial(Phone imsPhone, Context context)
+ public static void checkWfcWifiOnlyModeBeforeDial(Phone imsPhone, int phoneId, Context context)
throws CallStateException {
if (imsPhone == null || !imsPhone.isWifiCallingEnabled()) {
- boolean wfcWiFiOnly = (ImsManager.isWfcEnabledByPlatform(context) &&
- ImsManager.isWfcEnabledByUser(context) &&
- (ImsManager.getWfcMode(context) ==
- ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY));
+ ImsManager imsManager = ImsManager.getInstance(context, phoneId);
+ boolean wfcWiFiOnly = (imsManager.isWfcEnabledByPlatform()
+ && imsManager.isWfcEnabledByUser() && (imsManager.getWfcMode()
+ == ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY));
if (wfcWiFiOnly) {
throw new CallStateException(
CallStateException.ERROR_OUT_OF_SERVICE,
diff --git a/com/android/internal/telephony/PhoneInternalInterface.java b/com/android/internal/telephony/PhoneInternalInterface.java
index 4c39a654..feb3d68c 100644
--- a/com/android/internal/telephony/PhoneInternalInterface.java
+++ b/com/android/internal/telephony/PhoneInternalInterface.java
@@ -747,12 +747,17 @@ public interface PhoneInternalInterface {
/**
* @return true if user has enabled data
*/
- boolean getDataEnabled();
+ boolean isUserDataEnabled();
+
+ /**
+ * @return true if data is enabled considering all factors
+ */
+ boolean isDataEnabled();
/**
* @param @enable set {@code true} if enable data connection
*/
- void setDataEnabled(boolean enable);
+ void setUserDataEnabled(boolean enable);
/**
* Retrieves the unique device ID, e.g., IMEI for GSM phones and MEID for CDMA phones.
diff --git a/com/android/internal/telephony/RIL.java b/com/android/internal/telephony/RIL.java
index 2d334982..57c42265 100644
--- a/com/android/internal/telephony/RIL.java
+++ b/com/android/internal/telephony/RIL.java
@@ -53,6 +53,7 @@ import android.hardware.radio.V1_0.SimApdu;
import android.hardware.radio.V1_0.SmsWriteArgs;
import android.hardware.radio.V1_0.UusInfo;
import android.net.ConnectivityManager;
+import android.net.NetworkUtils;
import android.os.AsyncResult;
import android.os.Build;
import android.os.Handler;
@@ -81,7 +82,9 @@ import android.telephony.SignalStrength;
import android.telephony.SmsManager;
import android.telephony.TelephonyHistogram;
import android.telephony.TelephonyManager;
+import android.telephony.data.DataCallResponse;
import android.telephony.data.DataProfile;
+import android.telephony.data.InterfaceAddress;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
@@ -91,10 +94,10 @@ import com.android.internal.telephony.cat.ComprehensionTlv;
import com.android.internal.telephony.cat.ComprehensionTlvTag;
import com.android.internal.telephony.cdma.CdmaInformationRecords;
import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
-import com.android.internal.telephony.dataconnection.DataCallResponse;
import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.telephony.nano.TelephonyProto.SmsSession;
+import com.android.internal.telephony.uicc.IccSlotStatus;
import com.android.internal.telephony.uicc.IccUtils;
import java.io.ByteArrayInputStream;
@@ -102,6 +105,8 @@ import java.io.DataInputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -115,6 +120,7 @@ import java.util.concurrent.atomic.AtomicLong;
*/
public class RIL extends BaseCommands implements CommandsInterface {
static final String RILJ_LOG_TAG = "RILJ";
+ static final String RILJ_WAKELOCK_TAG = "*telephony-radio*";
// Have a separate wakelock instance for Ack
static final String RILJ_ACK_WAKELOCK_NAME = "RILJ_ACK_WL";
static final boolean RILJ_LOGD = true;
@@ -419,7 +425,7 @@ public class RIL extends BaseCommands implements CommandsInterface {
mRadioProxyDeathRecipient = new RadioProxyDeathRecipient();
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
- mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, RILJ_LOG_TAG);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, RILJ_WAKELOCK_TAG);
mWakeLock.setReferenceCounted(false);
mAckWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, RILJ_ACK_WAKELOCK_NAME);
mAckWakeLock.setReferenceCounted(false);
@@ -499,6 +505,69 @@ public class RIL extends BaseCommands implements CommandsInterface {
}
@Override
+ public void getIccSlotsStatus(Message result) {
+ IRadio radioProxy = getRadioProxy(result);
+ if (radioProxy != null) {
+ android.hardware.radio.V1_2.IRadio radioProxy12 =
+ android.hardware.radio.V1_2.IRadio.castFrom(radioProxy);
+ if (radioProxy12 == null) {
+ if (result != null) {
+ AsyncResult.forMessage(result, null,
+ CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+ result.sendToTarget();
+ }
+ } else {
+ RILRequest rr = obtainRequest(RIL_REQUEST_GET_SLOT_STATUS, result,
+ mRILDefaultWorkSource);
+
+ if (RILJ_LOGD) {
+ riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+ }
+
+ try {
+ radioProxy12.getSimSlotsStatus(rr.mSerial);
+ } catch (RemoteException | RuntimeException e) {
+ handleRadioProxyExceptionForRR(rr, "getIccSlotStatus", e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void setLogicalToPhysicalSlotMapping(int[] physicalSlots, Message result) {
+ IRadio radioProxy = getRadioProxy(result);
+ if (radioProxy != null) {
+ android.hardware.radio.V1_2.IRadio radioProxy12 =
+ android.hardware.radio.V1_2.IRadio.castFrom(radioProxy);
+ if (radioProxy12 == null) {
+ if (result != null) {
+ AsyncResult.forMessage(result, null,
+ CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+ result.sendToTarget();
+ }
+ } else {
+ ArrayList<Integer> mapping = new ArrayList<>();
+ for (int slot : physicalSlots) {
+ mapping.add(new Integer(slot));
+ }
+
+ RILRequest rr = obtainRequest(RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING,
+ result, mRILDefaultWorkSource);
+
+ if (RILJ_LOGD) {
+ riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+ }
+
+ try {
+ radioProxy12.setSimSlotsMapping(rr.mSerial, mapping);
+ } catch (RemoteException | RuntimeException e) {
+ handleRadioProxyExceptionForRR(rr, "setLogicalToPhysicalSlotMapping", e);
+ }
+ }
+ }
+ }
+
+ @Override
public void supplyIccPin(String pin, Message result) {
supplyIccPinForApp(pin, null, result);
}
@@ -1114,16 +1183,84 @@ public class RIL extends BaseCommands implements CommandsInterface {
* @return converted DataCallResponse object
*/
static DataCallResponse convertDataCallResult(SetupDataCallResult dcResult) {
+
+ // Process address
+ String[] addresses = null;
+ if (!TextUtils.isEmpty(dcResult.addresses)) {
+ addresses = dcResult.addresses.split(" ");
+ }
+
+ List<InterfaceAddress> iaList = new ArrayList<>();
+ if (addresses != null) {
+ for (String address : addresses) {
+ address = address.trim();
+ if (address.isEmpty()) continue;
+
+ String[] ap = address.split("/");
+ int addrPrefixLen = 0;
+ if (ap.length == 2) {
+ addrPrefixLen = Integer.parseInt(ap[1]);
+ }
+
+ try {
+ InterfaceAddress ia = new InterfaceAddress(ap[0], addrPrefixLen);
+ iaList.add(ia);
+ } catch (UnknownHostException e) {
+ Rlog.e(RILJ_LOG_TAG, "Unknown host exception: " + e);
+ }
+ }
+ }
+
+ // Process dns
+ String[] dnses = null;
+ if (!TextUtils.isEmpty(dcResult.dnses)) {
+ dnses = dcResult.dnses.split(" ");
+ }
+
+ List<InetAddress> dnsList = new ArrayList<>();
+ if (dnses != null) {
+ for (String dns : dnses) {
+ dns = dns.trim();
+ InetAddress ia;
+ try {
+ ia = NetworkUtils.numericToInetAddress(dns);
+ dnsList.add(ia);
+ } catch (IllegalArgumentException e) {
+ Rlog.e(RILJ_LOG_TAG, "Unknown dns: " + dns + ", exception = " + e);
+ }
+ }
+ }
+
+ // Process gateway
+ String[] gateways = null;
+ if (!TextUtils.isEmpty(dcResult.gateways)) {
+ gateways = dcResult.gateways.split(" ");
+ }
+
+ List<InetAddress> gatewayList = new ArrayList<>();
+ if (gateways != null) {
+ for (String gateway : gateways) {
+ gateway = gateway.trim();
+ InetAddress ia;
+ try {
+ ia = NetworkUtils.numericToInetAddress(gateway);
+ gatewayList.add(ia);
+ } catch (IllegalArgumentException e) {
+ Rlog.e(RILJ_LOG_TAG, "Unknown gateway: " + gateway + ", exception = " + e);
+ }
+ }
+ }
+
return new DataCallResponse(dcResult.status,
dcResult.suggestedRetryTime,
dcResult.cid,
dcResult.active,
dcResult.type,
dcResult.ifname,
- dcResult.addresses,
- dcResult.dnses,
- dcResult.gateways,
- dcResult.pcscf,
+ iaList,
+ dnsList,
+ gatewayList,
+ new ArrayList<>(Arrays.asList(dcResult.pcscf.trim().split("\\s*,\\s*"))),
dcResult.mtu
);
}
@@ -1611,9 +1748,9 @@ public class RIL extends BaseCommands implements CommandsInterface {
RadioAccessSpecifier ras) {
android.hardware.radio.V1_1.RadioAccessSpecifier rasInHalFormat =
new android.hardware.radio.V1_1.RadioAccessSpecifier();
- rasInHalFormat.radioAccessNetwork = ras.radioAccessNetwork;
+ rasInHalFormat.radioAccessNetwork = ras.getRadioAccessNetwork();
List<Integer> bands = null;
- switch (ras.radioAccessNetwork) {
+ switch (ras.getRadioAccessNetwork()) {
case RadioAccessNetworks.GERAN:
bands = rasInHalFormat.geranBands;
break;
@@ -1624,18 +1761,18 @@ public class RIL extends BaseCommands implements CommandsInterface {
bands = rasInHalFormat.eutranBands;
break;
default:
- Log.wtf(RILJ_LOG_TAG, "radioAccessNetwork " + ras.radioAccessNetwork
+ Log.wtf(RILJ_LOG_TAG, "radioAccessNetwork " + ras.getRadioAccessNetwork()
+ " not supported!");
return null;
}
- if (ras.bands != null) {
- for (int band : ras.bands) {
+ if (ras.getBands() != null) {
+ for (int band : ras.getBands()) {
bands.add(band);
}
}
- if (ras.channels != null) {
- for (int channel : ras.channels) {
+ if (ras.getChannels() != null) {
+ for (int channel : ras.getChannels()) {
rasInHalFormat.channels.add(channel);
}
}
@@ -1652,13 +1789,13 @@ public class RIL extends BaseCommands implements CommandsInterface {
if (radioProxy12 != null) {
android.hardware.radio.V1_2.NetworkScanRequest request =
new android.hardware.radio.V1_2.NetworkScanRequest();
- request.type = nsr.scanType;
- request.interval = nsr.searchPeriodicity;
- request.maxSearchTime = nsr.maxSearchTime;
- request.incrementalResultsPeriodicity = nsr.incrementalResultsPeriodicity;
- request.incrementalResults = nsr.incrementalResults;
+ request.type = nsr.getScanType();
+ request.interval = nsr.getSearchPeriodicity();
+ request.maxSearchTime = nsr.getMaxSearchTime();
+ request.incrementalResultsPeriodicity = nsr.getIncrementalResultsPeriodicity();
+ request.incrementalResults = nsr.getIncrementalResults();
- for (RadioAccessSpecifier ras : nsr.specifiers) {
+ for (RadioAccessSpecifier ras : nsr.getSpecifiers()) {
android.hardware.radio.V1_1.RadioAccessSpecifier rasInHalFormat =
convertRadioAccessSpecifierToRadioHAL(ras);
@@ -1669,7 +1806,7 @@ public class RIL extends BaseCommands implements CommandsInterface {
request.specifiers.add(rasInHalFormat);
}
- request.mccMncs.addAll(nsr.mccMncs);
+ request.mccMncs.addAll(nsr.getPlmns());
RILRequest rr = obtainRequest(RIL_REQUEST_START_NETWORK_SCAN, result,
mRILDefaultWorkSource);
@@ -1694,9 +1831,9 @@ public class RIL extends BaseCommands implements CommandsInterface {
} else {
android.hardware.radio.V1_1.NetworkScanRequest request =
new android.hardware.radio.V1_1.NetworkScanRequest();
- request.type = nsr.scanType;
- request.interval = nsr.searchPeriodicity;
- for (RadioAccessSpecifier ras : nsr.specifiers) {
+ request.type = nsr.getScanType();
+ request.interval = nsr.getSearchPeriodicity();
+ for (RadioAccessSpecifier ras : nsr.getSpecifiers()) {
android.hardware.radio.V1_1.RadioAccessSpecifier rasInHalFormat =
convertRadioAccessSpecifierToRadioHAL(ras);
if (rasInHalFormat == null) {
@@ -4589,6 +4726,10 @@ public class RIL extends BaseCommands implements CommandsInterface {
return "RIL_REQUEST_START_NETWORK_SCAN";
case RIL_REQUEST_STOP_NETWORK_SCAN:
return "RIL_REQUEST_STOP_NETWORK_SCAN";
+ case RIL_REQUEST_GET_SLOT_STATUS:
+ return "RIL_REQUEST_GET_SLOT_STATUS";
+ case RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING:
+ return "RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING";
default: return "<unknown request>";
}
}
@@ -4691,6 +4832,8 @@ public class RIL extends BaseCommands implements CommandsInterface {
return "RIL_UNSOL_CARRIER_INFO_IMSI_ENCRYPTION";
case RIL_UNSOL_NETWORK_SCAN_RESULT:
return "RIL_UNSOL_NETWORK_SCAN_RESULT";
+ case RIL_UNSOL_ICC_SLOT_STATUS:
+ return "RIL_UNSOL_ICC_SLOT_STATUS";
default:
return "<unknown response>";
}
@@ -4930,6 +5073,28 @@ public class RIL extends BaseCommands implements CommandsInterface {
}
/**
+ * Convert SlotsStatus defined in 1.2/types.hal to IccSlotStatus type.
+ * @param slotsStatus SlotsStatus defined in 1.2/types.hal
+ * @return Converted IccSlotStatus object
+ */
+ @VisibleForTesting
+ public static ArrayList<IccSlotStatus> convertHalSlotsStatus(
+ ArrayList<android.hardware.radio.V1_2.SimSlotStatus> slotsStatus) {
+ ArrayList<IccSlotStatus> iccSlotStatus = new ArrayList<IccSlotStatus>(slotsStatus.size());
+
+ for (android.hardware.radio.V1_2.SimSlotStatus slotStatus : slotsStatus) {
+ IccSlotStatus iss = new IccSlotStatus();
+ iss.setCardState(slotStatus.cardState);
+ iss.setSlotState(slotStatus.slotState);
+ iss.logicalSlotIndex = slotStatus.logicalSlotId;
+ iss.atr = slotStatus.atr;
+ iss.iccid = slotStatus.iccid;
+ iccSlotStatus.add(iss);
+ }
+ return iccSlotStatus;
+ }
+
+ /**
* Convert CellInfo defined in 1.0/types.hal to CellInfo type.
* @param records List of CellInfo defined in 1.0/types.hal
* @return List of converted CellInfo object
diff --git a/com/android/internal/telephony/RILConstants.java b/com/android/internal/telephony/RILConstants.java
index e2d25b8e..f804cb06 100644
--- a/com/android/internal/telephony/RILConstants.java
+++ b/com/android/internal/telephony/RILConstants.java
@@ -417,6 +417,8 @@ cat include/telephony/ril.h | \
int RIL_REQUEST_SET_CARRIER_INFO_IMSI_ENCRYPTION = 141;
int RIL_REQUEST_START_NETWORK_SCAN = 142;
int RIL_REQUEST_STOP_NETWORK_SCAN = 143;
+ int RIL_REQUEST_GET_SLOT_STATUS = 144;
+ int RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING = 145;
int RIL_RESPONSE_ACKNOWLEDGEMENT = 800;
@@ -471,4 +473,5 @@ cat include/telephony/ril.h | \
int RIL_UNSOL_MODEM_RESTART = 1047;
int RIL_UNSOL_CARRIER_INFO_IMSI_ENCRYPTION = 1048;
int RIL_UNSOL_NETWORK_SCAN_RESULT = 1049;
+ int RIL_UNSOL_ICC_SLOT_STATUS = 1050;
}
diff --git a/com/android/internal/telephony/RadioIndication.java b/com/android/internal/telephony/RadioIndication.java
index bcae450f..1e360c0f 100644
--- a/com/android/internal/telephony/RadioIndication.java
+++ b/com/android/internal/telephony/RadioIndication.java
@@ -28,6 +28,7 @@ import static com.android.internal.telephony.RILConstants.RIL_UNSOL_DATA_CALL_LI
import static com.android.internal.telephony.RILConstants.RIL_UNSOL_ENTER_EMERGENCY_CALLBACK_MODE;
import static com.android.internal.telephony.RILConstants.RIL_UNSOL_EXIT_EMERGENCY_CALLBACK_MODE;
import static com.android.internal.telephony.RILConstants.RIL_UNSOL_HARDWARE_CONFIG_CHANGED;
+import static com.android.internal.telephony.RILConstants.RIL_UNSOL_ICC_SLOT_STATUS;
import static com.android.internal.telephony.RILConstants.RIL_UNSOL_LCEDATA_RECV;
import static com.android.internal.telephony.RILConstants.RIL_UNSOL_MODEM_RESTART;
import static com.android.internal.telephony.RILConstants.RIL_UNSOL_NETWORK_SCAN_RESULT;
@@ -80,23 +81,24 @@ import android.hardware.radio.V1_0.SimRefreshResult;
import android.hardware.radio.V1_0.SsInfoData;
import android.hardware.radio.V1_0.StkCcUnsolSsResult;
import android.hardware.radio.V1_0.SuppSvcNotification;
-import android.hardware.radio.V1_1.IRadioIndication;
import android.hardware.radio.V1_1.KeepaliveStatus;
+import android.hardware.radio.V1_2.IRadioIndication;
import android.os.AsyncResult;
import android.os.SystemProperties;
import android.telephony.CellInfo;
import android.telephony.PcoData;
import android.telephony.SignalStrength;
import android.telephony.SmsMessage;
+import android.telephony.data.DataCallResponse;
import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
import com.android.internal.telephony.cdma.CdmaInformationRecords;
import com.android.internal.telephony.cdma.SmsMessageConverter;
-import com.android.internal.telephony.dataconnection.DataCallResponse;
import com.android.internal.telephony.gsm.SsData;
import com.android.internal.telephony.gsm.SuppServiceNotification;
import com.android.internal.telephony.nano.TelephonyProto.SmsSession;
import com.android.internal.telephony.uicc.IccRefreshResponse;
+import com.android.internal.telephony.uicc.IccSlotStatus;
import com.android.internal.telephony.uicc.IccUtils;
import java.util.ArrayList;
@@ -631,7 +633,24 @@ public class RadioIndication extends IRadioIndication.Stub {
if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_CELL_INFO_LIST, response);
- mRil.mRilCellInfoListRegistrants.notifyRegistrants(new AsyncResult (null, response, null));
+ mRil.mRilCellInfoListRegistrants.notifyRegistrants(new AsyncResult(null, response, null));
+ }
+
+ /**
+ * Indicates a change of the ICC slot status
+ * @param indicationType RadioIndicationType
+ * @param slotsStatus ICC slot status
+ */
+ public void simSlotsStatusChanged(int indicationType,
+ ArrayList<android.hardware.radio.V1_2.SimSlotStatus> slotsStatus) {
+ mRil.processIndication(indicationType);
+
+ ArrayList<IccSlotStatus> iccSlotStatus = RIL.convertHalSlotsStatus(slotsStatus);
+
+ if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_ICC_SLOT_STATUS, iccSlotStatus);
+
+ mRil.mIccStatusChangedRegistrants.notifyRegistrants(
+ new AsyncResult(null, iccSlotStatus, null));
}
/** Incremental network scan results */
diff --git a/com/android/internal/telephony/RadioResponse.java b/com/android/internal/telephony/RadioResponse.java
index 84c1d573..53f4f91b 100644
--- a/com/android/internal/telephony/RadioResponse.java
+++ b/com/android/internal/telephony/RadioResponse.java
@@ -33,8 +33,9 @@ import android.hardware.radio.V1_0.RadioResponseInfo;
import android.hardware.radio.V1_0.SendSmsResult;
import android.hardware.radio.V1_0.SetupDataCallResult;
import android.hardware.radio.V1_0.VoiceRegStateResult;
-import android.hardware.radio.V1_1.IRadioResponse;
import android.hardware.radio.V1_1.KeepaliveStatus;
+import android.hardware.radio.V1_2.IRadioResponse;
+import android.hardware.radio.V1_2.SimSlotStatus;
import android.os.AsyncResult;
import android.os.Message;
import android.os.SystemClock;
@@ -46,13 +47,14 @@ import android.telephony.PhoneNumberUtils;
import android.telephony.SignalStrength;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.telephony.data.DataCallResponse;
import android.text.TextUtils;
-import com.android.internal.telephony.dataconnection.DataCallResponse;
import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
import com.android.internal.telephony.uicc.IccCardApplicationStatus;
import com.android.internal.telephony.uicc.IccCardStatus;
import com.android.internal.telephony.uicc.IccIoResult;
+import com.android.internal.telephony.uicc.IccSlotStatus;
import com.android.internal.telephony.uicc.IccUtils;
import java.util.ArrayList;
@@ -105,6 +107,31 @@ public class RadioResponse extends IRadioResponse.Stub {
/**
* @param responseInfo Response info struct containing response type, serial no. and error
+ * @param cardStatus ICC card status as defined by CardStatus in 1.2/types.hal
+ */
+ public void getIccCardStatusResponse_1_2(RadioResponseInfo responseInfo,
+ android.hardware.radio.V1_2.CardStatus cardStatus) {
+ responseIccCardStatus_1_2(responseInfo, cardStatus);
+ }
+
+ /**
+ * @param responseInfo Response info struct containing response type, serial no. and error
+ * @param slotsStatus ICC slot status as defined by SlotsStatus in 1.2/types.hal
+ */
+ public void getSimSlotsStatusResponse(RadioResponseInfo responseInfo,
+ ArrayList<SimSlotStatus> slotsStatus) {
+ responseIccSlotStatus(responseInfo, slotsStatus);
+ }
+
+ /**
+ * @param responseInfo Response info struct containing response type, serial no. and error
+ */
+ public void setSimSlotsMappingResponse(RadioResponseInfo responseInfo) {
+ responseVoid(responseInfo);
+ }
+
+ /**
+ * @param responseInfo Response info struct containing response type, serial no. and error
* @param remainingAttempts Number of retries remaining, must be equal to -1 if unknown.
*/
public void supplyIccPinForAppResponse(RadioResponseInfo responseInfo, int remainingAttempts) {
@@ -1241,39 +1268,61 @@ public class RadioResponse extends IRadioResponse.Stub {
throw new UnsupportedOperationException("stopKeepaliveResponse not implemented");
}
+ private IccCardStatus convertHalCardStatus(CardStatus cardStatus) {
+ IccCardStatus iccCardStatus = new IccCardStatus();
+ iccCardStatus.setCardState(cardStatus.cardState);
+ iccCardStatus.setUniversalPinState(cardStatus.universalPinState);
+ iccCardStatus.mGsmUmtsSubscriptionAppIndex = cardStatus.gsmUmtsSubscriptionAppIndex;
+ iccCardStatus.mCdmaSubscriptionAppIndex = cardStatus.cdmaSubscriptionAppIndex;
+ iccCardStatus.mImsSubscriptionAppIndex = cardStatus.imsSubscriptionAppIndex;
+ int numApplications = cardStatus.applications.size();
+
+ // limit to maximum allowed applications
+ if (numApplications
+ > com.android.internal.telephony.uicc.IccCardStatus.CARD_MAX_APPS) {
+ numApplications =
+ com.android.internal.telephony.uicc.IccCardStatus.CARD_MAX_APPS;
+ }
+ iccCardStatus.mApplications = new IccCardApplicationStatus[numApplications];
+ for (int i = 0; i < numApplications; i++) {
+ AppStatus rilAppStatus = cardStatus.applications.get(i);
+ IccCardApplicationStatus appStatus = new IccCardApplicationStatus();
+ appStatus.app_type = appStatus.AppTypeFromRILInt(rilAppStatus.appType);
+ appStatus.app_state = appStatus.AppStateFromRILInt(rilAppStatus.appState);
+ appStatus.perso_substate = appStatus.PersoSubstateFromRILInt(
+ rilAppStatus.persoSubstate);
+ appStatus.aid = rilAppStatus.aidPtr;
+ appStatus.app_label = rilAppStatus.appLabelPtr;
+ appStatus.pin1_replaced = rilAppStatus.pin1Replaced;
+ appStatus.pin1 = appStatus.PinStateFromRILInt(rilAppStatus.pin1);
+ appStatus.pin2 = appStatus.PinStateFromRILInt(rilAppStatus.pin2);
+ iccCardStatus.mApplications[i] = appStatus;
+ }
+ return iccCardStatus;
+ }
+
private void responseIccCardStatus(RadioResponseInfo responseInfo, CardStatus cardStatus) {
RILRequest rr = mRil.processResponse(responseInfo);
if (rr != null) {
- IccCardStatus iccCardStatus = new IccCardStatus();
- iccCardStatus.setCardState(cardStatus.cardState);
- iccCardStatus.setUniversalPinState(cardStatus.universalPinState);
- iccCardStatus.mGsmUmtsSubscriptionAppIndex = cardStatus.gsmUmtsSubscriptionAppIndex;
- iccCardStatus.mCdmaSubscriptionAppIndex = cardStatus.cdmaSubscriptionAppIndex;
- iccCardStatus.mImsSubscriptionAppIndex = cardStatus.imsSubscriptionAppIndex;
- int numApplications = cardStatus.applications.size();
-
- // limit to maximum allowed applications
- if (numApplications
- > com.android.internal.telephony.uicc.IccCardStatus.CARD_MAX_APPS) {
- numApplications =
- com.android.internal.telephony.uicc.IccCardStatus.CARD_MAX_APPS;
- }
- iccCardStatus.mApplications = new IccCardApplicationStatus[numApplications];
- for (int i = 0; i < numApplications; i++) {
- AppStatus rilAppStatus = cardStatus.applications.get(i);
- IccCardApplicationStatus appStatus = new IccCardApplicationStatus();
- appStatus.app_type = appStatus.AppTypeFromRILInt(rilAppStatus.appType);
- appStatus.app_state = appStatus.AppStateFromRILInt(rilAppStatus.appState);
- appStatus.perso_substate = appStatus.PersoSubstateFromRILInt(
- rilAppStatus.persoSubstate);
- appStatus.aid = rilAppStatus.aidPtr;
- appStatus.app_label = rilAppStatus.appLabelPtr;
- appStatus.pin1_replaced = rilAppStatus.pin1Replaced;
- appStatus.pin1 = appStatus.PinStateFromRILInt(rilAppStatus.pin1);
- appStatus.pin2 = appStatus.PinStateFromRILInt(rilAppStatus.pin2);
- iccCardStatus.mApplications[i] = appStatus;
+ IccCardStatus iccCardStatus = convertHalCardStatus(cardStatus);
+ mRil.riljLog("responseIccCardStatus: from HIDL: " + iccCardStatus);
+ if (responseInfo.error == RadioError.NONE) {
+ sendMessageResponse(rr.mResult, iccCardStatus);
}
+ mRil.processResponseDone(rr, responseInfo, iccCardStatus);
+ }
+ }
+
+ private void responseIccCardStatus_1_2(RadioResponseInfo responseInfo,
+ android.hardware.radio.V1_2.CardStatus cardStatus) {
+ RILRequest rr = mRil.processResponse(responseInfo);
+
+ if (rr != null) {
+ IccCardStatus iccCardStatus = convertHalCardStatus(cardStatus.base);
+ iccCardStatus.physicalSlotIndex = cardStatus.physicalSlotId;
+ iccCardStatus.atr = cardStatus.atr;
+ iccCardStatus.iccid = cardStatus.iccid;
mRil.riljLog("responseIccCardStatus: from HIDL: " + iccCardStatus);
if (responseInfo.error == RadioError.NONE) {
sendMessageResponse(rr.mResult, iccCardStatus);
@@ -1282,6 +1331,20 @@ public class RadioResponse extends IRadioResponse.Stub {
}
}
+ private void responseIccSlotStatus(RadioResponseInfo responseInfo,
+ ArrayList<SimSlotStatus> slotsStatus) {
+ RILRequest rr = mRil.processResponse(responseInfo);
+ if (rr != null) {
+ ArrayList<IccSlotStatus> iccSlotStatus = RIL.convertHalSlotsStatus(slotsStatus);
+
+ mRil.riljLog("responseIccSlotStatus: from HIDL: " + iccSlotStatus);
+ if (responseInfo.error == RadioError.NONE) {
+ sendMessageResponse(rr.mResult, iccSlotStatus);
+ }
+ mRil.processResponseDone(rr, responseInfo, iccSlotStatus);
+ }
+ }
+
private void responseInts(RadioResponseInfo responseInfo, int ...var) {
final ArrayList<Integer> ints = new ArrayList<>();
for (int i = 0; i < var.length; i++) {
diff --git a/com/android/internal/telephony/SMSDispatcher.java b/com/android/internal/telephony/SMSDispatcher.java
index dc08ec02..2ec51010 100644
--- a/com/android/internal/telephony/SMSDispatcher.java
+++ b/com/android/internal/telephony/SMSDispatcher.java
@@ -17,8 +17,6 @@
package com.android.internal.telephony;
import static android.Manifest.permission.SEND_SMS_NO_CONFIRMATION;
-import static com.android.internal.telephony.IccSmsInterfaceManager.SMS_MESSAGE_PERIOD_NOT_SPECIFIED;
-import static com.android.internal.telephony.IccSmsInterfaceManager.SMS_MESSAGE_PRIORITY_NOT_SPECIFIED;
import static android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE;
import static android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE;
import static android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED;
@@ -799,29 +797,10 @@ public abstract class SMSDispatcher extends Handler {
* @param callingPkg the calling package name
* @param persistMessage whether to save the sent message into SMS DB for a
* non-default SMS app.
- *
- * @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.
*/
protected abstract void sendText(String destAddr, String scAddr, String text,
PendingIntent sentIntent, PendingIntent deliveryIntent, Uri messageUri,
- String callingPkg, boolean persistMessage, int priority, boolean expectMore,
- int validityPeriod);
+ String callingPkg, boolean persistMessage);
/**
* Inject an SMS PDU into the android platform.
@@ -873,28 +852,11 @@ public abstract class SMSDispatcher extends Handler {
* @param callingPkg the calling package name
* @param persistMessage whether to save the sent message into SMS DB for a
* non-default SMS app.
- * @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.
*/
protected void sendMultipartText(String destAddr, String scAddr,
ArrayList<String> parts, ArrayList<PendingIntent> sentIntents,
ArrayList<PendingIntent> deliveryIntents, Uri messageUri, String callingPkg,
- boolean persistMessage, int priority, boolean expectMore, int validityPeriod) {
+ boolean persistMessage) {
final String fullMessageText = getMultipartMessageText(parts);
int refNumber = getNextConcatenatedRef() & 0x00FF;
int msgCount = parts.size();
@@ -951,8 +913,7 @@ public abstract class SMSDispatcher extends Handler {
trackers[i] =
getNewSubmitPduTracker(destAddr, scAddr, parts.get(i), smsHeader, encoding,
sentIntent, deliveryIntent, (i == (msgCount - 1)),
- unsentPartCount, anyPartFailed, messageUri,
- fullMessageText, priority, expectMore, validityPeriod);
+ unsentPartCount, anyPartFailed, messageUri, fullMessageText);
trackers[i].mPersistMessage = persistMessage;
}
@@ -982,12 +943,11 @@ public abstract class SMSDispatcher extends Handler {
/**
* Create a new SubmitPdu and return the SMS tracker.
*/
- protected abstract SmsTracker getNewSubmitPduTracker(String destinationAddress,
- String scAddress, String message, SmsHeader smsHeader, int encoding,
+ protected abstract SmsTracker getNewSubmitPduTracker(String destinationAddress, String scAddress,
+ String message, SmsHeader smsHeader, int encoding,
PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart,
- AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed,
- Uri messageUri, String fullMessageText,
- int priority, boolean expectMore, int validityPeriod);
+ AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
+ String fullMessageText);
/**
* Send an SMS
@@ -1360,8 +1320,7 @@ public abstract class SMSDispatcher extends Handler {
}
sendMultipartText(destinationAddress, scAddress, parts, sentIntents, deliveryIntents,
- null/*messageUri*/, null/*callingPkg*/, tracker.mPersistMessage, tracker.mPriority,
- tracker.mExpectMore, tracker.mValidityPeriod);
+ null/*messageUri*/, null/*callingPkg*/, tracker.mPersistMessage);
}
/**
@@ -1375,8 +1334,6 @@ public abstract class SMSDispatcher extends Handler {
public int mImsRetry; // nonzero indicates initial message was sent over Ims
public int mMessageRef;
public boolean mExpectMore;
- public int mValidityPeriod;
- public int mPriority;
String mFormat;
public final PendingIntent mSentIntent;
@@ -1410,9 +1367,8 @@ public abstract class SMSDispatcher extends Handler {
private SmsTracker(HashMap<String, Object> data, PendingIntent sentIntent,
PendingIntent deliveryIntent, PackageInfo appInfo, String destAddr, String format,
AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
- SmsHeader smsHeader, boolean expectMore, String fullMessageText, int subId,
- boolean isText, boolean persistMessage, int userId, int priority,
- int validityPeriod) {
+ SmsHeader smsHeader, boolean isExpectMore, String fullMessageText, int subId,
+ boolean isText, boolean persistMessage, int userId) {
mData = data;
mSentIntent = sentIntent;
mDeliveryIntent = deliveryIntent;
@@ -1420,7 +1376,7 @@ public abstract class SMSDispatcher extends Handler {
mAppInfo = appInfo;
mDestAddress = destAddr;
mFormat = format;
- mExpectMore = expectMore;
+ mExpectMore = isExpectMore;
mImsRetry = 0;
mMessageRef = 0;
mUnsentPartCount = unsentPartCount;
@@ -1432,8 +1388,6 @@ public abstract class SMSDispatcher extends Handler {
mIsText = isText;
mPersistMessage = persistMessage;
mUserId = userId;
- mPriority = priority;
- mValidityPeriod = validityPeriod;
}
/**
@@ -1649,8 +1603,7 @@ public abstract class SMSDispatcher extends Handler {
protected SmsTracker getSmsTracker(HashMap<String, Object> data, PendingIntent sentIntent,
PendingIntent deliveryIntent, String format, AtomicInteger unsentPartCount,
AtomicBoolean anyPartFailed, Uri messageUri, SmsHeader smsHeader,
- boolean expectMore, String fullMessageText, boolean isText, boolean persistMessage,
- int priority, int validityPeriod) {
+ boolean isExpectMore, String fullMessageText, boolean isText, boolean persistMessage) {
// Get calling app package name via UID from Binder call
PackageManager pm = mContext.getPackageManager();
String[] packageNames = pm.getPackagesForUid(Binder.getCallingUid());
@@ -1671,27 +1624,16 @@ public abstract class SMSDispatcher extends Handler {
// and before displaying the number to the user if confirmation is required.
String destAddr = PhoneNumberUtils.extractNetworkPortion((String) data.get("destAddr"));
return new SmsTracker(data, sentIntent, deliveryIntent, appInfo, destAddr, format,
- unsentPartCount, anyPartFailed, messageUri, smsHeader, expectMore,
- fullMessageText, getSubId(), isText, persistMessage, userId, priority,
- validityPeriod);
+ unsentPartCount, anyPartFailed, messageUri, smsHeader, isExpectMore,
+ fullMessageText, getSubId(), isText, persistMessage, userId);
}
protected SmsTracker getSmsTracker(HashMap<String, Object> data, PendingIntent sentIntent,
- PendingIntent deliveryIntent, String format, Uri messageUri, boolean expectMore,
+ PendingIntent deliveryIntent, String format, Uri messageUri, boolean isExpectMore,
String fullMessageText, boolean isText, boolean persistMessage) {
return getSmsTracker(data, sentIntent, deliveryIntent, format, null/*unsentPartCount*/,
- null/*anyPartFailed*/, messageUri, null/*smsHeader*/, expectMore,
- fullMessageText, isText, persistMessage, SMS_MESSAGE_PRIORITY_NOT_SPECIFIED,
- SMS_MESSAGE_PERIOD_NOT_SPECIFIED);
- }
-
- protected SmsTracker getSmsTracker(HashMap<String, Object> data, PendingIntent sentIntent,
- PendingIntent deliveryIntent, String format, Uri messageUri, boolean expectMore,
- String fullMessageText, boolean isText, boolean persistMessage, int priority,
- int validityPeriod) {
- return getSmsTracker(data, sentIntent, deliveryIntent, format, null/*unsentPartCount*/,
- null/*anyPartFailed*/, messageUri, null/*smsHeader*/, expectMore, fullMessageText,
- isText, persistMessage, priority, validityPeriod);
+ null/*anyPartFailed*/, messageUri, null/*smsHeader*/, isExpectMore,
+ fullMessageText, isText, persistMessage);
}
protected HashMap<String, Object> getSmsTrackerMap(String destAddr, String scAddr,
diff --git a/com/android/internal/telephony/ServiceStateTracker.java b/com/android/internal/telephony/ServiceStateTracker.java
index a9523bbd..8d8f6dd2 100644
--- a/com/android/internal/telephony/ServiceStateTracker.java
+++ b/com/android/internal/telephony/ServiceStateTracker.java
@@ -32,7 +32,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
-import android.database.ContentObserver;
import android.hardware.radio.V1_0.CellInfoType;
import android.hardware.radio.V1_0.DataRegStateResult;
import android.hardware.radio.V1_0.RegState;
@@ -43,7 +42,6 @@ import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.PersistableBundle;
-import android.os.PowerManager;
import android.os.Registrant;
import android.os.RegistrantList;
import android.os.SystemClock;
@@ -212,37 +210,6 @@ public class ServiceStateTracker extends Handler {
protected static final int EVENT_IMS_SERVICE_STATE_CHANGED = 53;
protected static final int EVENT_RADIO_POWER_OFF_DONE = 54;
- protected static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
-
- /**
- * List of ISO codes for countries that can have an offset of
- * GMT+0 when not in daylight savings time. This ignores some
- * small places such as the Canary Islands (Spain) and
- * Danmarkshavn (Denmark). The list must be sorted by code.
- */
- protected static final String[] GMT_COUNTRY_CODES = {
- "bf", // Burkina Faso
- "ci", // Cote d'Ivoire
- "eh", // Western Sahara
- "fo", // Faroe Islands, Denmark
- "gb", // United Kingdom of Great Britain and Northern Ireland
- "gh", // Ghana
- "gm", // Gambia
- "gn", // Guinea
- "gw", // Guinea Bissau
- "ie", // Ireland
- "lr", // Liberia
- "is", // Iceland
- "ma", // Morocco
- "ml", // Mali
- "mr", // Mauritania
- "pt", // Portugal
- "sl", // Sierra Leone
- "sn", // Senegal
- "st", // Sao Tome and Principe
- "tg", // Togo
- };
-
private class CellInfoResult {
List<CellInfo> list;
Object lockObj = new Object();
@@ -284,8 +251,6 @@ public class ServiceStateTracker extends Handler {
private final LocalLog mPhoneTypeLog = new LocalLog(10);
private final LocalLog mRatLog = new LocalLog(20);
private final LocalLog mRadioPowerLog = new LocalLog(20);
- private final LocalLog mTimeLog = new LocalLog(15);
- private final LocalLog mTimeZoneLog = new LocalLog(15);
private class SstSubscriptionsChangedListener extends OnSubscriptionsChangedListener {
public final AtomicInteger mPreviousSubId =
@@ -368,42 +333,13 @@ public class ServiceStateTracker extends Handler {
};
//Common
- private GsmCdmaPhone mPhone;
+ private final GsmCdmaPhone mPhone;
+
public CellLocation mCellLoc;
private CellLocation mNewCellLoc;
- public static final int MS_PER_HOUR = 60 * 60 * 1000;
- /* Time stamp after 19 January 2038 is not supported under 32 bit */
- private static final int MAX_NITZ_YEAR = 2037;
- /**
- * Sometimes we get the NITZ time before we know what country we
- * are in. Keep the time zone information from the NITZ string so
- * we can fix the time zone once know the country.
- */
- private boolean mNeedFixZoneAfterNitz = false;
- private NitzData mNitzData;
- private boolean mGotCountryCode = false;
- private String mSavedTimeZone;
- private long mSavedTime;
- private long mSavedAtTime;
- /** Wake lock used while setting time of day. */
- private PowerManager.WakeLock mWakeLock;
- public static final String WAKELOCK_TAG = "ServiceStateTracker";
- private ContentResolver mCr;
- private ContentObserver mAutoTimeObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- Rlog.i(LOG_TAG, "Auto time state changed");
- revertToNitzTime();
- }
- };
-
- private ContentObserver mAutoTimeZoneObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- Rlog.i(LOG_TAG, "Auto time zone state changed");
- revertToNitzTimeZone();
- }
- };
+ private static final int MS_PER_HOUR = 60 * 60 * 1000;
+ private final NitzStateMachine mNitzState;
+ private final ContentResolver mCr;
//GSM
private int mPreferredNetworkType;
@@ -433,8 +369,6 @@ public class ServiceStateTracker extends Handler {
* Mark when service state is in emergency call only mode
*/
private boolean mEmergencyOnly = false;
- /** Boolean is true is setTimeFromNITZString was called */
- private boolean mNitzUpdatedTime = false;
/** Started the recheck process after finding gprs should registered but not. */
private boolean mStartedGprsRegCheck;
/** Already sent the event-log for no gprs register. */
@@ -493,14 +427,6 @@ public class ServiceStateTracker extends Handler {
public static final String UNACTIVATED_MIN_VALUE = "1111110111";
// Current Otasp value
private int mCurrentOtaspMode = TelephonyManager.OTASP_UNINITIALIZED;
- /** if time between NITZ updates is less than mNitzUpdateSpacing the update may be ignored. */
- public static final int NITZ_UPDATE_SPACING_DEFAULT = 1000 * 60 * 10;
- private int mNitzUpdateSpacing = SystemProperties.getInt("ro.nitz_update_spacing",
- NITZ_UPDATE_SPACING_DEFAULT);
- /** If mNitzUpdateSpacing hasn't been exceeded but update is > mNitzUpdate do the update */
- public static final int NITZ_UPDATE_DIFF_DEFAULT = 2000;
- private int mNitzUpdateDiff = SystemProperties.getInt("ro.nitz_update_diff",
- NITZ_UPDATE_DIFF_DEFAULT);
private int mRoamingIndicator;
private boolean mIsInPrl;
private int mDefaultRoamingIndicator;
@@ -536,6 +462,7 @@ public class ServiceStateTracker extends Handler {
private static final int INVALID_LTE_EARFCN = -1;
public ServiceStateTracker(GsmCdmaPhone phone, CommandsInterface ci) {
+ mNitzState = TelephonyComponentFactory.getInstance().makeNitzStateMachine(phone);
mPhone = phone;
mCi = ci;
@@ -555,11 +482,6 @@ public class ServiceStateTracker extends Handler {
mRestrictedState = new RestrictedState();
mCi.registerForImsNetworkStateChanged(this, EVENT_IMS_STATE_CHANGED, null);
-
- PowerManager powerManager =
- (PowerManager)phone.getContext().getSystemService(Context.POWER_SERVICE);
- mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
-
mCi.registerForRadioStateChanged(this, EVENT_RADIO_STATE_CHANGED, null);
mCi.registerForNetworkStateChanged(this, EVENT_NETWORK_STATE_CHANGED, null);
mCi.setOnNITZTime(this, EVENT_NITZ_TIME, null);
@@ -574,12 +496,6 @@ public class ServiceStateTracker extends Handler {
enableCellularOnBoot);
- mCr.registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
- mAutoTimeObserver);
- mCr.registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
- mAutoTimeZoneObserver);
setSignalStrengthDefaultValues();
mPhone.getCarrierActionAgent().registerForCarrierAction(CARRIER_ACTION_SET_RADIO_ENABLED,
this, EVENT_RADIO_POWER_FROM_CARRIER, null, false);
@@ -643,7 +559,7 @@ public class ServiceStateTracker extends Handler {
mMin = null;
mPrlVersion = null;
mIsMinInfoReady = false;
- mNitzUpdatedTime = false;
+ mNitzState.clearNitzUpdatedTime();
//cancel any pending pollstate request on voice tech switching
cancelPollState();
@@ -2631,8 +2547,8 @@ public class ServiceStateTracker extends Handler {
mNewSS.setStateOutOfService();
mNewCellLoc.setStateInvalid();
setSignalStrengthDefaultValues();
- mGotCountryCode = false;
- mNitzUpdatedTime = false;
+ mNitzState.setNetworkCountryIsoAvailable(false);
+ mNitzState.clearNitzUpdatedTime();
pollStateDone();
break;
@@ -2640,8 +2556,8 @@ public class ServiceStateTracker extends Handler {
mNewSS.setStateOff();
mNewCellLoc.setStateInvalid();
setSignalStrengthDefaultValues();
- mGotCountryCode = false;
- mNitzUpdatedTime = false;
+ mNitzState.setNetworkCountryIsoAvailable(false);
+ mNitzState.clearNitzUpdatedTime();
// don't poll when device is shutting down or the poll was not modemTrigged
// (they sent us new radio data) and current network is not IWLAN
if (mDeviceShuttingDown ||
@@ -2723,8 +2639,9 @@ public class ServiceStateTracker extends Handler {
boolean hasLocationChanged = !mNewCellLoc.equals(mCellLoc);
// ratchet the new tech up through it's rat family but don't drop back down
- // until cell change
- if (!hasLocationChanged) {
+ // until cell change or device is OOS
+ boolean isDataInService = mNewSS.getDataRegState() == ServiceState.STATE_IN_SERVICE;
+ if (!hasLocationChanged && isDataInService) {
mRatRatcheter.ratchetRat(mSS, mNewSS);
}
@@ -2873,10 +2790,11 @@ public class ServiceStateTracker extends Handler {
mNetworkAttachedRegistrants.notifyRegistrants();
if (DBG) {
- log("pollStateDone: registering current mNitzUpdatedTime=" + mNitzUpdatedTime
- + " changing to false");
+ log("pollStateDone: hasRegistered, current mNitzState.getNitzUpdatedTime()="
+ + mNitzState.getNitzUpdatedTime()
+ + ". Calling mNitzState.clearNitzUpdatedTime()");
}
- mNitzUpdatedTime = false;
+ mNitzState.clearNitzUpdatedTime();
}
if (hasDeregistered) {
@@ -2909,8 +2827,8 @@ public class ServiceStateTracker extends Handler {
if (isInvalidOperatorNumeric(operatorNumeric)) {
if (DBG) log("operatorNumeric " + operatorNumeric + " is invalid");
tm.setNetworkCountryIsoForPhone(mPhone.getPhoneId(), "");
- mGotCountryCode = false;
- mNitzUpdatedTime = false;
+ mNitzState.setNetworkCountryIsoAvailable(false);
+ mNitzState.clearNitzUpdatedTime();
} else if (mSS.getRilDataRadioTechnology() != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN) {
// Update time zone, ISO, and IDD.
//
@@ -2928,20 +2846,28 @@ public class ServiceStateTracker extends Handler {
}
tm.setNetworkCountryIsoForPhone(mPhone.getPhoneId(), iso);
- mGotCountryCode = true;
+ mNitzState.setNetworkCountryIsoAvailable(true);
- if (!mNitzUpdatedTime && !mcc.equals("000") && !TextUtils.isEmpty(iso)
- && getAutoTimeZone()) {
- updateTimeZoneByNetworkCountryCode(iso);
+ if (!mcc.equals("000")
+ && !TextUtils.isEmpty(iso)
+ && mNitzState.shouldUpdateTimeZoneUsingCountryCode()) {
+ mNitzState.updateTimeZoneByNetworkCountryCode(iso);
}
if (!mPhone.isPhoneTypeGsm()) {
setOperatorIdd(operatorNumeric);
}
- if (shouldFixTimeZoneNow(mPhone, operatorNumeric, prevOperatorNumeric,
- mNeedFixZoneAfterNitz)) {
- fixTimeZone(iso);
+ boolean mccChanged = mccChanged(operatorNumeric, prevOperatorNumeric);
+ boolean fixTimeZoneCallNeeded = mNitzState.fixTimeZoneCallNeeded();
+ if (mccChanged || fixTimeZoneCallNeeded) {
+ // fixTimeZoneCallNeeded == need to fix it because when the NITZ time
+ // came in we didn't know the country code.
+ if (DBG) {
+ log("shouldFixTimeZoneNow: mccChanged=" + mccChanged
+ + " fixTimeZoneCallNeeded=" + fixTimeZoneCallNeeded);
+ }
+ mNitzState.fixTimeZone(iso);
}
}
@@ -3056,7 +2982,8 @@ public class ServiceStateTracker extends Handler {
if (!hasBrandOverride && (mCi.getRadioState().isOn()) && (mPhone.isEriFileLoaded()) &&
(!ServiceState.isLte(mSS.getRilVoiceRadioTechnology()) ||
mPhone.getContext().getResources().getBoolean(com.android.internal.R.
- bool.config_LTE_eri_for_network_name))) {
+ bool.config_LTE_eri_for_network_name)) &&
+ (!mIsSubscriptionFromRuim)) {
// Only when CDMA is in service, ERI will take effect
String eriText = mSS.getOperatorAlpha();
// Now the Phone sees the new ServiceState so it can get the new ERI text
@@ -3151,112 +3078,42 @@ public class ServiceStateTracker extends Handler {
}
// resolve the mcc from sid;
- // if mSavedTimeZone is null, TimeZone would get the default timeZone,
- // and the fixTimeZone couldn't help, because it depends on operator Numeric;
+ // if mNitzState.getSavedTimeZoneId() is null, TimeZone would get the default timeZone,
+ // and the mNitzState.fixTimeZone() couldn't help, because it depends on operator Numeric;
// if the sid is conflict and timezone is unavailable, the mcc may be not right.
boolean isNitzTimeZone;
- int rawUtcOffsetMillis;
- if (mSavedTimeZone != null) {
- rawUtcOffsetMillis = TimeZone.getTimeZone(mSavedTimeZone).getRawOffset();
+ TimeZone tzone;
+ if (mNitzState.getSavedTimeZoneId() != null) {
+ tzone = TimeZone.getTimeZone(mNitzState.getSavedTimeZoneId());
isNitzTimeZone = true;
} else {
- rawUtcOffsetMillis = 0;
- if (mNitzData != null) {
- TimeZone tzone = NitzData.guessTimeZone(mNitzData);
- if (tzone != null) {
- rawUtcOffsetMillis = tzone.getRawOffset();
+ NitzData lastNitzData = mNitzState.getCachedNitzData();
+ if (lastNitzData == null) {
+ tzone = null;
+ } else {
+ tzone = NitzData.guessTimeZone(lastNitzData);
+ if (ServiceStateTracker.DBG) {
+ log("fixUnknownMcc(): guessNitzTimeZone returned "
+ + (tzone == null ? tzone : tzone.getID()));
}
}
isNitzTimeZone = false;
}
- int rawUtcOffsetHours = rawUtcOffsetMillis / MS_PER_HOUR;
- boolean isDst = mNitzData != null && mNitzData.isDst();
- int mcc = mHbpcdUtils.getMcc(sid, rawUtcOffsetHours, (isDst ? 1 : 0), isNitzTimeZone);
+ int utcOffsetHours = 0;
+ if (tzone != null) {
+ utcOffsetHours = tzone.getRawOffset() / MS_PER_HOUR;
+ }
+
+ NitzData nitzData = mNitzState.getCachedNitzData();
+ boolean isDst = nitzData != null && nitzData.isDst();
+ int mcc = mHbpcdUtils.getMcc(sid, utcOffsetHours, (isDst ? 1 : 0), isNitzTimeZone);
if (mcc > 0) {
operatorNumeric = Integer.toString(mcc) + DEFAULT_MNC;
}
return operatorNumeric;
}
- private void fixTimeZone(String isoCountryCode) {
- TimeZone zone = null;
- // If the offset is (0, false) and the time zone property
- // is set, use the time zone property rather than GMT.
- final String zoneName = SystemProperties.get(TIMEZONE_PROPERTY);
- if (DBG) {
- log("fixTimeZone zoneName='" + zoneName
- + "' mNitzData=" + mNitzData
- + " iso-cc='" + isoCountryCode
- + "' iso-cc-idx=" + Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode));
- }
- if ("".equals(isoCountryCode) && mNeedFixZoneAfterNitz) {
- // Country code not found. This is likely a test network.
- // Get a TimeZone based only on the NITZ parameters (best guess).
-
- // mNeedFixZoneAfterNitz is only set to true when mNitzData is set so there's no need to
- // check mNitzData == null.
- zone = NitzData.guessTimeZone(mNitzData);
- if (DBG) log("pollStateDone: using NITZ TimeZone");
- } else if ((mNitzData == null
- || (mNitzData.getLocalOffsetMillis() == 0 && !mNitzData.isDst()))
- && (zoneName != null) && (zoneName.length() > 0)
- && (Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode) < 0)) {
- // For NITZ string without time zone,
- // need adjust time to reflect default time zone setting
- zone = TimeZone.getDefault();
- if (mNeedFixZoneAfterNitz) {
- long ctm = System.currentTimeMillis();
- long tzOffset = zone.getOffset(ctm);
- if (DBG) {
- log("fixTimeZone: tzOffset=" + tzOffset +
- " ltod=" + TimeUtils.logTimeOfDay(ctm));
- }
- if (getAutoTime()) {
- long adj = ctm - tzOffset;
- if (DBG) log("fixTimeZone: adj ltod=" + TimeUtils.logTimeOfDay(adj));
- setAndBroadcastNetworkSetTime(adj);
- } else {
- // Adjust the saved NITZ time to account for tzOffset.
- mSavedTime = mSavedTime - tzOffset;
- if (DBG) log("fixTimeZone: adj mSavedTime=" + mSavedTime);
- }
- }
- if (DBG) log("fixTimeZone: using default TimeZone");
- } else {
- if (mNitzData == null) {
- // This behavior is unusual but consistent with historical behavior when it wasn't
- // possible to detect whether a previous NITZ signal had been saved.
- zone = TimeUtils.getTimeZone(0 /* offset */, false /* dst */, 0 /* when */,
- isoCountryCode);
- } else {
- zone = TimeUtils.getTimeZone(mNitzData.getLocalOffsetMillis(), mNitzData.isDst(),
- mNitzData.getCurrentTimeInMillis(), isoCountryCode);
- }
- if (DBG) log("fixTimeZone: using getTimeZone(off, dst, time, iso)");
- }
-
- final String tmpLog = "fixTimeZone zoneName=" + zoneName + " mNitzData=" + mNitzData
- + " iso-cc=" + isoCountryCode + " mNeedFixZoneAfterNitz="
- + mNeedFixZoneAfterNitz + " zone=" + (zone != null ? zone.getID() : "NULL");
- mTimeZoneLog.log(tmpLog);
-
- if (zone != null) {
- log("fixTimeZone: zone != null zone.getID=" + zone.getID());
- if (getAutoTimeZone()) {
- setAndBroadcastNetworkSetTimeZone(zone.getID());
- } else {
- log("fixTimeZone: skip changing zone as getAutoTimeZone was false");
- }
- if (mNeedFixZoneAfterNitz) {
- saveNitzTimeZone(zone.getID());
- }
- } else {
- log("fixTimeZone: zone == null, do nothing for zone");
- }
- mNeedFixZoneAfterNitz = false;
- }
-
/**
* Check if GPRS got registered while voice is registered.
*
@@ -3566,272 +3423,21 @@ public class ServiceStateTracker extends Handler {
/**
* nitzReceiveTime is time_t that the NITZ time was posted
*/
- private void setTimeFromNITZString (String nitz, long nitzReceiveTime) {
- // "yy/mm/dd,hh:mm:ss(+/-)tz"
- // tz is in number of quarter-hours
-
+ private void setTimeFromNITZString(String nitzString, long nitzReceiveTime) {
long start = SystemClock.elapsedRealtime();
if (DBG) {
- log("NITZ: " + nitz + "," + nitzReceiveTime
+ Rlog.d(LOG_TAG, "NITZ: " + nitzString + "," + nitzReceiveTime
+ " start=" + start + " delay=" + (start - nitzReceiveTime));
}
-
- NitzData nitzData = NitzData.parse(nitz);
- if (nitzData == null) {
- return;
- }
-
- try {
- String iso = ((TelephonyManager) mPhone.getContext().
- getSystemService(Context.TELEPHONY_SERVICE)).
- getNetworkCountryIsoForPhone(mPhone.getPhoneId());
-
- TimeZone zone = nitzData.getEmulatorHostTimeZone();
- if (zone == null) {
- if (mGotCountryCode) {
- if (iso != null && iso.length() > 0) {
- zone = TimeUtils.getTimeZone(
- nitzData.getLocalOffsetMillis(),
- nitzData.isDst(),
- nitzData.getCurrentTimeInMillis(),
- iso);
- } else {
- // We don't have a valid iso country code. This is
- // most likely because we're on a test network that's
- // using a bogus MCC (eg, "001"), so get a TimeZone
- // based only on the NITZ parameters.
- zone = NitzData.guessTimeZone(nitzData);
- }
- }
- }
-
- int previousUtcOffset;
- boolean previousIsDst;
- if (mNitzData == null) {
- // No previously saved NITZ data. Use the same defaults as Android would have done
- // before it was possible to detect this case.
- previousUtcOffset = 0;
- previousIsDst = false;
- } else {
- previousUtcOffset = mNitzData.getLocalOffsetMillis();
- previousIsDst = mNitzData.isDst();
- }
- if ((zone == null)
- || (nitzData.getLocalOffsetMillis() != previousUtcOffset)
- || (nitzData.isDst() != previousIsDst)) {
- // We got the time before the country or the zone has changed
- // so we don't know how to identify the DST rules yet. Save
- // the information and hope to fix it up later.
-
- mNeedFixZoneAfterNitz = true;
- mNitzData = nitzData;
- }
-
- String tmpLog = "NITZ: nitzData=" + nitzData + " nitzReceiveTime=" + nitzReceiveTime
- + " zone=" + (zone != null ? zone.getID() : "NULL")
- + " iso=" + iso + " mGotCountryCode=" + mGotCountryCode
- + " mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz
- + " getAutoTimeZone()=" + getAutoTimeZone();
- if (DBG) {
- log(tmpLog);
- }
- mTimeZoneLog.log(tmpLog);
-
- if (zone != null) {
- if (getAutoTimeZone()) {
- setAndBroadcastNetworkSetTimeZone(zone.getID());
- }
- saveNitzTimeZone(zone.getID());
- }
-
- String ignore = SystemProperties.get("gsm.ignore-nitz");
- if (ignore != null && ignore.equals("yes")) {
- log("NITZ: Not setting clock because gsm.ignore-nitz is set");
- return;
- }
-
+ NitzData newNitzData = NitzData.parse(nitzString);
+ if (newNitzData != null) {
try {
- mWakeLock.acquire();
-
- long millisSinceNitzReceived = SystemClock.elapsedRealtime() - nitzReceiveTime;
- if (millisSinceNitzReceived < 0) {
- // Sanity check: something is wrong
- if (DBG) {
- log("NITZ: not setting time, clock has rolled "
- + "backwards since NITZ time was received, "
- + nitz);
- }
- return;
- }
-
- if (millisSinceNitzReceived > Integer.MAX_VALUE) {
- // If the time is this far off, something is wrong > 24 days!
- if (DBG) {
- log("NITZ: not setting time, processing has taken "
- + (millisSinceNitzReceived / (1000 * 60 * 60 * 24))
- + " days");
- }
- return;
- }
-
- // Adjust the NITZ time by the delay since it was received.
- long adjustedCurrentTimeMillis = nitzData.getCurrentTimeInMillis();
- adjustedCurrentTimeMillis += millisSinceNitzReceived;
-
- if (!mPhone.isPhoneTypeGsm() || getAutoTime()) {
- tmpLog = "NITZ: nitz=" + nitz + " nitzReceiveTime=" + nitzReceiveTime
- + " Setting time of day to " + adjustedCurrentTimeMillis
- + " NITZ receive delay(ms): " + millisSinceNitzReceived
- + " gained(ms): "
- + (adjustedCurrentTimeMillis - System.currentTimeMillis())
- + " from " + nitz;
- if (DBG) {
- log(tmpLog);
- }
- mTimeLog.log(tmpLog);
- if (mPhone.isPhoneTypeGsm()) {
- setAndBroadcastNetworkSetTime(adjustedCurrentTimeMillis);
- Rlog.i(LOG_TAG, "NITZ: after Setting time of day");
- } else {
- if (getAutoTime()) {
- /**
- * Update system time automatically
- */
- long gained = adjustedCurrentTimeMillis - System.currentTimeMillis();
- long timeSinceLastUpdate = SystemClock.elapsedRealtime() - mSavedAtTime;
- int nitzUpdateSpacing = Settings.Global.getInt(mCr,
- Settings.Global.NITZ_UPDATE_SPACING, mNitzUpdateSpacing);
- int nitzUpdateDiff = Settings.Global.getInt(mCr,
- Settings.Global.NITZ_UPDATE_DIFF, mNitzUpdateDiff);
-
- if ((mSavedAtTime == 0) || (timeSinceLastUpdate > nitzUpdateSpacing)
- || (Math.abs(gained) > nitzUpdateDiff)) {
- if (DBG) {
- log("NITZ: Auto updating time of day to "
- + adjustedCurrentTimeMillis
- + " NITZ receive delay=" + millisSinceNitzReceived
- + "ms gained=" + gained + "ms from " + nitz);
- }
-
- setAndBroadcastNetworkSetTime(adjustedCurrentTimeMillis);
- } else {
- if (DBG) {
- log("NITZ: ignore, a previous update was "
- + timeSinceLastUpdate + "ms ago and gained=" + gained + "ms");
- }
- return;
- }
- }
- }
- }
- SystemProperties.set("gsm.nitz.time", String.valueOf(adjustedCurrentTimeMillis));
- saveNitzTime(adjustedCurrentTimeMillis);
- mNitzUpdatedTime = true;
+ mNitzState.setTimeAndTimeZoneFromNitz(newNitzData, nitzReceiveTime);
} finally {
if (DBG) {
long end = SystemClock.elapsedRealtime();
- log("NITZ: end=" + end + " dur=" + (end - start));
+ Rlog.d(LOG_TAG, "NITZ: end=" + end + " dur=" + (end - start));
}
- mWakeLock.release();
- }
- } catch (RuntimeException ex) {
- loge("NITZ: Parsing NITZ time " + nitz + " ex=" + ex);
- }
- }
-
- private boolean getAutoTime() {
- try {
- return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME) > 0;
- } catch (Settings.SettingNotFoundException snfe) {
- return true;
- }
- }
-
- private boolean getAutoTimeZone() {
- try {
- return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE) > 0;
- } catch (Settings.SettingNotFoundException snfe) {
- return true;
- }
- }
-
- private void saveNitzTimeZone(String zoneId) {
- mSavedTimeZone = zoneId;
- }
-
- private void saveNitzTime(long time) {
- mSavedTime = time;
- mSavedAtTime = SystemClock.elapsedRealtime();
- }
-
- /**
- * Set the timezone and send out a sticky broadcast so the system can
- * determine if the timezone was set by the carrier.
- *
- * @param zoneId timezone set by carrier
- */
- private void setAndBroadcastNetworkSetTimeZone(String zoneId) {
- if (DBG) log("setAndBroadcastNetworkSetTimeZone: setTimeZone=" + zoneId);
- AlarmManager alarm =
- (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE);
- alarm.setTimeZone(zoneId);
- Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE);
- intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
- intent.putExtra("time-zone", zoneId);
- mPhone.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
- if (DBG) {
- log("setAndBroadcastNetworkSetTimeZone: call alarm.setTimeZone and broadcast zoneId=" +
- zoneId);
- }
- }
-
- /**
- * Set the time and Send out a sticky broadcast so the system can determine
- * if the time was set by the carrier.
- *
- * @param time time set by network
- */
- private void setAndBroadcastNetworkSetTime(long time) {
- if (DBG) log("setAndBroadcastNetworkSetTime: time=" + time + "ms");
- SystemClock.setCurrentTimeMillis(time);
- Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME);
- intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
- intent.putExtra("time", time);
- mPhone.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
-
- TelephonyMetrics.getInstance().writeNITZEvent(mPhone.getPhoneId(), time);
- }
-
- private void revertToNitzTime() {
- if (Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME, 0) == 0) {
- return;
- }
- if (DBG) {
- log("Reverting to NITZ Time: mSavedTime=" + mSavedTime + " mSavedAtTime=" +
- mSavedAtTime);
- }
- if (mSavedTime != 0 && mSavedAtTime != 0) {
- long currTime = SystemClock.elapsedRealtime();
- mTimeLog.log("Reverting to NITZ time, currTime=" + currTime
- + " mSavedAtTime=" + mSavedAtTime + " mSavedTime=" + mSavedTime);
- setAndBroadcastNetworkSetTime(mSavedTime + (currTime - mSavedAtTime));
- }
- }
-
- private void revertToNitzTimeZone() {
- if (Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE, 0) == 0) {
- return;
- }
- String tmpLog = "Reverting to NITZ TimeZone: tz=" + mSavedTimeZone;
- if (DBG) log(tmpLog);
- mTimeZoneLog.log(tmpLog);
- if (mSavedTimeZone != null) {
- setAndBroadcastNetworkSetTimeZone(mSavedTimeZone);
- } else {
- String iso = ((TelephonyManager) mPhone.getContext().getSystemService(
- Context.TELEPHONY_SERVICE)).getNetworkCountryIsoForPhone(mPhone.getPhoneId());
- if (!TextUtils.isEmpty(iso)) {
- updateTimeZoneByNetworkCountryCode(iso);
}
}
}
@@ -3845,7 +3451,9 @@ public class ServiceStateTracker extends Handler {
if (DBG) log("setNotification: cancelAllNotifications");
NotificationManager notificationManager = (NotificationManager)
mPhone.getContext().getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancelAll();
+ notificationManager.cancel(PS_NOTIFICATION);
+ notificationManager.cancel(CS_NOTIFICATION);
+ notificationManager.cancel(CS_REJECT_CAUSE_NOTIFICATION);
}
/**
@@ -4425,20 +4033,11 @@ public class ServiceStateTracker extends Handler {
}
/**
- * Return true if time zone needs fixing.
- *
- * @param phone
- * @param operatorNumeric
- * @param prevOperatorNumeric
- * @param needToFixTimeZone
- * @return true if time zone needs to be fixed
+ * Return true if the operator changed.
*/
- private boolean shouldFixTimeZoneNow(Phone phone, String operatorNumeric,
- String prevOperatorNumeric, boolean needToFixTimeZone) {
+ private boolean mccChanged(String operatorNumeric, String prevOperatorNumeric) {
// Return false if the mcc isn't valid as we don't know where we are.
- // Return true if we have an IccCard and the mcc changed or we
- // need to fix it because when the NITZ time came in we didn't
- // know the country code.
+ // Return true if we have an IccCard and the mcc changed.
// If mcc is invalid then we'll return false
int mcc;
@@ -4446,8 +4045,7 @@ public class ServiceStateTracker extends Handler {
mcc = Integer.parseInt(operatorNumeric.substring(0, 3));
} catch (Exception e) {
if (DBG) {
- log("shouldFixTimeZoneNow: no mcc, operatorNumeric=" + operatorNumeric +
- " retVal=false");
+ log("mccChanged: no mcc, operatorNumeric=" + operatorNumeric + " retVal=false");
}
return false;
}
@@ -4468,14 +4066,13 @@ public class ServiceStateTracker extends Handler {
}
// Determine retVal
- boolean retVal = ((iccCardExist && (mcc != prevMcc)) || needToFixTimeZone);
+ boolean retVal = iccCardExist && (mcc != prevMcc);
if (DBG) {
long ctm = System.currentTimeMillis();
log("shouldFixTimeZoneNow: retVal=" + retVal +
" iccCardExist=" + iccCardExist +
" operatorNumeric=" + operatorNumeric + " mcc=" + mcc +
" prevOperatorNumeric=" + prevOperatorNumeric + " prevMcc=" + prevMcc +
- " needToFixTimeZone=" + needToFixTimeZone +
" ltod=" + TimeUtils.logTimeOfDay(ctm));
}
return retVal;
@@ -4650,18 +4247,12 @@ public class ServiceStateTracker extends Handler {
pw.println(" mGsmRoaming=" + mGsmRoaming);
pw.println(" mDataRoaming=" + mDataRoaming);
pw.println(" mEmergencyOnly=" + mEmergencyOnly);
- pw.println(" mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz);
pw.flush();
- pw.println(" mNitzData=" + mNitzData);
- pw.println(" mGotCountryCode=" + mGotCountryCode);
- pw.println(" mNitzUpdatedTime=" + mNitzUpdatedTime);
- pw.println(" mSavedTimeZone=" + mSavedTimeZone);
- pw.println(" mSavedTime=" + mSavedTime);
- pw.println(" mSavedAtTime=" + mSavedAtTime);
+ mNitzState.dumpState(pw);
+ pw.flush();
pw.println(" mStartedGprsRegCheck=" + mStartedGprsRegCheck);
pw.println(" mReportedGprsNoReg=" + mReportedGprsNoReg);
pw.println(" mNotification=" + mNotification);
- pw.println(" mWakeLock=" + mWakeLock);
pw.println(" mCurSpn=" + mCurSpn);
pw.println(" mCurDataSpn=" + mCurDataSpn);
pw.println(" mCurShowSpn=" + mCurShowSpn);
@@ -4720,15 +4311,7 @@ public class ServiceStateTracker extends Handler {
ipw.increaseIndent();
mRadioPowerLog.dump(fd, ipw, args);
- ipw.println(" Time Logs:");
- ipw.increaseIndent();
- mTimeLog.dump(fd, ipw, args);
- ipw.decreaseIndent();
-
- ipw.println(" Time zone Logs:");
- ipw.increaseIndent();
- mTimeZoneLog.dump(fd, ipw, args);
- ipw.decreaseIndent();
+ mNitzState.dumpLogs(fd, ipw, args);
}
public boolean isImsRegistered() {
@@ -5004,37 +4587,6 @@ public class ServiceStateTracker extends Handler {
}
/**
- * Update time zone by network country code, works on countries which only have one time zone.
- * @param iso Country code from network MCC
- */
- private void updateTimeZoneByNetworkCountryCode(String iso) {
- // Test both paths if ignore nitz is true
- boolean testOneUniqueOffsetPath = SystemProperties.getBoolean(
- TelephonyProperties.PROPERTY_IGNORE_NITZ, false)
- && ((SystemClock.uptimeMillis() & 1) == 0);
-
- List<String> uniqueZoneIds = TimeUtils.getTimeZoneIdsWithUniqueOffsets(iso);
- if ((uniqueZoneIds.size() == 1) || testOneUniqueOffsetPath) {
- String zoneId = uniqueZoneIds.get(0);
- if (DBG) {
- log("updateTimeZoneByNetworkCountryCode: no nitz but one TZ for iso-cc=" + iso
- + " with zone.getID=" + zoneId
- + " testOneUniqueOffsetPath=" + testOneUniqueOffsetPath);
- }
- mTimeZoneLog.log("updateTimeZoneByNetworkCountryCode: set time zone=" + zoneId
- + " iso=" + iso);
- setAndBroadcastNetworkSetTimeZone(zoneId);
- } else {
- if (DBG) {
- log("updateTimeZoneByNetworkCountryCode: there are " + uniqueZoneIds.size()
- + " unique offsets for iso-cc='" + iso
- + " testOneUniqueOffsetPath=" + testOneUniqueOffsetPath
- + "', do nothing");
- }
- }
- }
-
- /**
* Check whether to use only RSRP for the number of LTE signal bar.
*
* @return true if it should use only RSRP for the number of LTE signal bar.
diff --git a/com/android/internal/telephony/SubscriptionInfoUpdater.java b/com/android/internal/telephony/SubscriptionInfoUpdater.java
index 983c436f..57082d31 100644
--- a/com/android/internal/telephony/SubscriptionInfoUpdater.java
+++ b/com/android/internal/telephony/SubscriptionInfoUpdater.java
@@ -51,8 +51,6 @@ import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.euicc.EuiccController;
import com.android.internal.telephony.uicc.IccCardProxy;
-import com.android.internal.telephony.uicc.IccConstants;
-import com.android.internal.telephony.uicc.IccFileHandler;
import com.android.internal.telephony.uicc.IccRecords;
import com.android.internal.telephony.uicc.IccUtils;
@@ -68,7 +66,6 @@ public class SubscriptionInfoUpdater extends Handler {
private static final String LOG_TAG = "SubscriptionInfoUpdater";
private static final int PROJECT_SIM_NUM = TelephonyManager.getDefault().getPhoneCount();
- private static final int EVENT_SIM_LOCKED_QUERY_ICCID_DONE = 1;
private static final int EVENT_GET_NETWORK_SELECTION_MODE_DONE = 2;
private static final int EVENT_SIM_LOADED = 3;
private static final int EVENT_SIM_ABSENT = 4;
@@ -240,61 +237,9 @@ public class SubscriptionInfoUpdater extends Handler {
return true;
}
- public void setDisplayNameForNewSub(String newSubName, int subId, int newNameSource) {
- SubscriptionInfo subInfo = mSubscriptionManager.getActiveSubscriptionInfo(subId);
- if (subInfo != null) {
- // overwrite SIM display name if it is not assigned by user
- int oldNameSource = subInfo.getNameSource();
- CharSequence oldSubName = subInfo.getDisplayName();
- logd("[setDisplayNameForNewSub] subId = " + subInfo.getSubscriptionId()
- + ", oldSimName = " + oldSubName + ", oldNameSource = " + oldNameSource
- + ", newSubName = " + newSubName + ", newNameSource = " + newNameSource);
- if (oldSubName == null ||
- (oldNameSource ==
- SubscriptionManager.NAME_SOURCE_DEFAULT_SOURCE && newSubName != null) ||
- (oldNameSource == SubscriptionManager.NAME_SOURCE_SIM_SOURCE && newSubName != null
- && !newSubName.equals(oldSubName))) {
- mSubscriptionManager.setDisplayName(newSubName, subInfo.getSubscriptionId(),
- newNameSource);
- }
- } else {
- logd("SUB" + (subId + 1) + " SubInfo not created yet");
- }
- }
-
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case EVENT_SIM_LOCKED_QUERY_ICCID_DONE: {
- AsyncResult ar = (AsyncResult)msg.obj;
- QueryIccIdUserObj uObj = (QueryIccIdUserObj) ar.userObj;
- int slotId = uObj.slotId;
- logd("handleMessage : <EVENT_SIM_LOCKED_QUERY_ICCID_DONE> SIM" + (slotId + 1));
- if (ar.exception == null) {
- if (ar.result != null) {
- byte[] data = (byte[])ar.result;
- mIccId[slotId] = stripIccIdSuffix(
- IccUtils.bchToString(data, 0, data.length));
- } else {
- logd("Null ar");
- mIccId[slotId] = ICCID_STRING_FOR_NO_SIM;
- }
- } else {
- mIccId[slotId] = ICCID_STRING_FOR_NO_SIM;
- logd("Query IccId fail: " + ar.exception);
- }
- logd("sIccId[" + slotId + "] = " + mIccId[slotId]);
- if (isAllIccIdQueryDone()) {
- updateSubscriptionInfoByIccId();
- }
- broadcastSimStateChanged(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED,
- uObj.reason);
- if (!ICCID_STRING_FOR_NO_SIM.equals(mIccId[slotId])) {
- updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED);
- }
- break;
- }
-
case EVENT_GET_NETWORK_SELECTION_MODE_DONE: {
AsyncResult ar = (AsyncResult)msg.obj;
Integer slotId = (Integer)ar.userObj;
@@ -367,25 +312,24 @@ public class SubscriptionInfoUpdater extends Handler {
mIccId[slotId] = null;
}
-
- IccFileHandler fileHandler = mPhone[slotId].getIccCard() == null ? null :
- mPhone[slotId].getIccCard().getIccFileHandler();
-
- if (fileHandler != null) {
- String iccId = mIccId[slotId];
- if (iccId == null) {
- logd("Querying IccId");
- fileHandler.loadEFTransparent(IccConstants.EF_ICCID,
- obtainMessage(EVENT_SIM_LOCKED_QUERY_ICCID_DONE,
- new QueryIccIdUserObj(reason, slotId)));
- } else {
- logd("NOT Querying IccId its already set sIccid[" + slotId + "]=" + iccId);
- updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED);
- broadcastSimStateChanged(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED, reason);
+ String iccId = mIccId[slotId];
+ if (iccId == null) {
+ IccRecords records = mPhone[slotId].getIccCard().getIccRecords();
+ if (stripIccIdSuffix(records.getFullIccId()) == null) {
+ logd("handleSimLocked: IccID null");
+ return;
}
+ mIccId[slotId] = stripIccIdSuffix(records.getFullIccId());
} else {
- logd("sFh[" + slotId + "] is null, ignore");
+ logd("NOT Querying IccId its already set sIccid[" + slotId + "]=" + iccId);
+ }
+
+ if (isAllIccIdQueryDone()) {
+ updateSubscriptionInfoByIccId();
}
+
+ updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED);
+ broadcastSimStateChanged(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED, reason);
}
private void handleSimLoaded(int slotId) {
@@ -401,7 +345,7 @@ public class SubscriptionInfoUpdater extends Handler {
return;
}
if (stripIccIdSuffix(records.getFullIccId()) == null) {
- logd("onRecieve: IccID null");
+ logd("handleSimLoaded: IccID null");
return;
}
mIccId[slotId] = stripIccIdSuffix(records.getFullIccId());
diff --git a/com/android/internal/telephony/TelephonyComponentFactory.java b/com/android/internal/telephony/TelephonyComponentFactory.java
index 82a08238..aeed9dee 100644
--- a/com/android/internal/telephony/TelephonyComponentFactory.java
+++ b/com/android/internal/telephony/TelephonyComponentFactory.java
@@ -61,6 +61,20 @@ public class TelephonyComponentFactory {
return new ServiceStateTracker(phone, ci);
}
+ /**
+ * Returns a new {@link NitzStateMachine} instance.
+ */
+ public NitzStateMachine makeNitzStateMachine(GsmCdmaPhone phone) {
+ return new NitzStateMachine(phone);
+ }
+
+ /**
+ * Returns a new {@link TimeServiceHelper} instance.
+ */
+ public TimeServiceHelper makeTimeServiceHelper(Context context) {
+ return new TimeServiceHelper(context);
+ }
+
public SimActivationTracker makeSimActivationTracker(Phone phone) {
return new SimActivationTracker(phone);
}
diff --git a/com/android/internal/telephony/TimeServiceHelper.java b/com/android/internal/telephony/TimeServiceHelper.java
new file mode 100644
index 00000000..fb1efd05
--- /dev/null
+++ b/com/android/internal/telephony/TimeServiceHelper.java
@@ -0,0 +1,179 @@
+/*
+ * 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 com.android.internal.telephony;
+
+import android.app.AlarmManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+/**
+ * An interface to various time / time zone detection behaviors that should be centralized into a
+ * new service.
+ */
+// Non-final to allow mocking.
+public class TimeServiceHelper {
+
+ /**
+ * Callback interface for automatic detection enable/disable changes.
+ */
+ public interface Listener {
+ /**
+ * Automatic time detection has been enabled or disabled.
+ */
+ void onTimeDetectionChange(boolean enabled);
+
+ /**
+ * Automatic time zone detection has been enabled or disabled.
+ */
+ void onTimeZoneDetectionChange(boolean enabled);
+ }
+
+ private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
+
+ private final Context mContext;
+ private final ContentResolver mCr;
+
+ private Listener mListener;
+
+ /** Creates a TimeServiceHelper */
+ public TimeServiceHelper(Context context) {
+ mContext = context;
+ mCr = context.getContentResolver();
+ }
+
+ /**
+ * Sets a listener that will be called when the automatic time / time zone detection setting
+ * changes.
+ */
+ public void setListener(Listener listener) {
+ if (listener == null) {
+ throw new NullPointerException("listener==null");
+ }
+ if (mListener != null) {
+ throw new IllegalStateException("listener already set");
+ }
+ this.mListener = listener;
+ mCr.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
+ new ContentObserver(new Handler()) {
+ public void onChange(boolean selfChange) {
+ listener.onTimeDetectionChange(isTimeDetectionEnabled());
+ }
+ });
+ mCr.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
+ new ContentObserver(new Handler()) {
+ public void onChange(boolean selfChange) {
+ listener.onTimeZoneDetectionChange(isTimeZoneDetectionEnabled());
+ }
+ });
+ }
+
+ /**
+ * Returns true if the device has an explicit time zone set.
+ */
+ public boolean isTimeZoneSettingInitialized() {
+ return isTimeZoneSettingInitializedStatic();
+
+ }
+
+ /**
+ * Returns true if automatic time detection is enabled in settings.
+ */
+ public boolean isTimeDetectionEnabled() {
+ try {
+ return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME) > 0;
+ } catch (Settings.SettingNotFoundException snfe) {
+ return true;
+ }
+ }
+
+ /**
+ * Returns true if automatic time zone detection is enabled in settings.
+ */
+ public boolean isTimeZoneDetectionEnabled() {
+ try {
+ return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE) > 0;
+ } catch (Settings.SettingNotFoundException snfe) {
+ return true;
+ }
+ }
+
+ /**
+ * Set the device time zone and send out a sticky broadcast so the system can
+ * determine if the timezone was set by the carrier.
+ *
+ * @param zoneId timezone set by carrier
+ */
+ public void setDeviceTimeZone(String zoneId) {
+ setDeviceTimeZoneStatic(mContext, zoneId);
+ }
+
+ /**
+ * Set the time and Send out a sticky broadcast so the system can determine
+ * if the time was set by the carrier.
+ *
+ * @param time time set by network
+ */
+ public void setDeviceTime(long time) {
+ SystemClock.setCurrentTimeMillis(time);
+ Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+ intent.putExtra("time", time);
+ mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ /**
+ * Static implementation of isTimeZoneSettingInitialized() for use from {@link MccTable}. This
+ * is a hack to deflake TelephonyTests when running on a device with a real SIM: in that
+ * situation real service events may come in while a TelephonyTest is running, leading to flakes
+ * as the real / fake instance of TimeServiceHelper is swapped in and out from
+ * {@link TelephonyComponentFactory}.
+ */
+ static boolean isTimeZoneSettingInitializedStatic() {
+ // timezone.equals("GMT") will be true and only true if the timezone was
+ // set to a default value by the system server (when starting, system server
+ // sets the persist.sys.timezone to "GMT" if it's not set). "GMT" is not used by
+ // any code that sets it explicitly (in case where something sets GMT explicitly,
+ // "Etc/GMT" Olsen ID would be used).
+ // TODO(b/64056758): Remove "timezone.equals("GMT")" hack when there's a
+ // better way of telling if the value has been defaulted.
+
+ String timeZoneId = SystemProperties.get(TIMEZONE_PROPERTY);
+ return timeZoneId != null && timeZoneId.length() > 0 && !timeZoneId.equals("GMT");
+ }
+
+ /**
+ * Static method for use by MccTable. See {@link #isTimeZoneSettingInitializedStatic()} for
+ * explanation.
+ */
+ static void setDeviceTimeZoneStatic(Context context, String zoneId) {
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ alarmManager.setTimeZone(zoneId);
+ Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+ intent.putExtra("time-zone", zoneId);
+ context.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+}
diff --git a/com/android/internal/telephony/UiccSmsController.java b/com/android/internal/telephony/UiccSmsController.java
index 59449b83..e6cb972d 100644
--- a/com/android/internal/telephony/UiccSmsController.java
+++ b/com/android/internal/telephony/UiccSmsController.java
@@ -159,21 +159,6 @@ public class UiccSmsController extends ISms.Stub {
}
}
- @Override
- public void sendTextForSubscriberWithOptions(int subId, String callingPackage,
- String destAddr, String scAddr, String parts, PendingIntent sentIntents,
- PendingIntent deliveryIntents, boolean persistMessage, int priority,
- boolean expectMore, int validityPeriod) {
- IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
- if (iccSmsIntMgr != null ) {
- iccSmsIntMgr.sendTextWithOptions(callingPackage, destAddr, scAddr, parts, sentIntents,
- deliveryIntents, persistMessage, priority, expectMore, validityPeriod);
- } else {
- Rlog.e(LOG_TAG,"sendTextWithOptions iccSmsIntMgr is null for" +
- " Subscription: " + subId);
- }
- }
-
public void sendMultipartText(String callingPackage, String destAddr, String scAddr,
List<String> parts, List<PendingIntent> sentIntents,
List<PendingIntent> deliveryIntents) throws android.os.RemoteException {
@@ -199,22 +184,6 @@ public class UiccSmsController extends ISms.Stub {
}
@Override
- public void sendMultipartTextForSubscriberWithOptions(int subId, String callingPackage,
- String destAddr, String scAddr, List<String> parts, List<PendingIntent> sentIntents,
- List<PendingIntent> deliveryIntents, boolean persistMessage, int priority,
- boolean expectMore, int validityPeriod) {
- IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);
- if (iccSmsIntMgr != null ) {
- iccSmsIntMgr.sendMultipartTextWithOptions(callingPackage, destAddr, scAddr, parts,
- sentIntents, deliveryIntents, persistMessage, priority, expectMore,
- validityPeriod);
- } else {
- Rlog.e(LOG_TAG,"sendMultipartTextWithOptions iccSmsIntMgr is null for" +
- " Subscription: " + subId);
- }
- }
-
- @Override
public boolean enableCellBroadcastForSubscriber(int subId, int messageIdentifier, int ranType)
throws android.os.RemoteException {
return enableCellBroadcastRangeForSubscriber(subId, messageIdentifier, messageIdentifier,
diff --git a/com/android/internal/telephony/cat/CatService.java b/com/android/internal/telephony/cat/CatService.java
index 802944d8..0c79118c 100644
--- a/com/android/internal/telephony/cat/CatService.java
+++ b/com/android/internal/telephony/cat/CatService.java
@@ -625,10 +625,12 @@ public class CatService extends Handler implements AppInterface {
CommandType cmdType = AppInterface.CommandType.fromInt(cmdDet.typeOfCommand);
if (cmdType != null) {
switch (cmdType) {
+ case GET_INPUT:
case GET_INKEY:
- // ETSI TS 102 384,27.22.4.2.8.4.2.
- // If it is a response for GET_INKEY command and the response timeout
- // occured, then add DURATION TLV for variable timeout case.
+ // Please refer to the clause 6.8.21 of ETSI 102.223.
+ // The terminal shall supply the command execution duration
+ // when it issues TERMINAL RESPONSE for GET INKEY command with variable timeout.
+ // GET INPUT command should also be handled in the same manner.
if ((resultCode.value() == ResultCode.NO_RESPONSE_FROM_USER.value()) &&
(cmdInput != null) && (cmdInput.duration != null)) {
getInKeyResponse(buf, cmdInput);
diff --git a/com/android/internal/telephony/cat/CommandParamsFactory.java b/com/android/internal/telephony/cat/CommandParamsFactory.java
index 256f541e..232f8081 100644
--- a/com/android/internal/telephony/cat/CommandParamsFactory.java
+++ b/com/android/internal/telephony/cat/CommandParamsFactory.java
@@ -549,6 +549,11 @@ class CommandParamsFactory extends Handler {
input.iconSelfExplanatory = iconId.selfExplanatory;
}
+ ctlv = searchForTag(ComprehensionTlvTag.DURATION, ctlvs);
+ if (ctlv != null) {
+ input.duration = ValueParser.retrieveDuration(ctlv);
+ }
+
input.digitOnly = (cmdDet.commandQualifier & 0x01) == 0;
input.ucs2 = (cmdDet.commandQualifier & 0x02) != 0;
input.echo = (cmdDet.commandQualifier & 0x04) == 0;
diff --git a/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java b/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
index 080ca1c3..1cfdc334 100644
--- a/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
+++ b/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
@@ -113,7 +113,7 @@ public class CdmaSMSDispatcher extends SMSDispatcher {
if (pdu != null) {
HashMap map = getSmsTrackerMap(destAddr, scAddr, destPort, data, pdu);
SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
- null /*messageUri*/, false /*expectMore*/, null /*fullMessageText*/,
+ null /*messageUri*/, false /*isExpectMore*/, null /*fullMessageText*/,
false /*isText*/, true /*persistMessage*/);
String carrierPackage = getCarrierAppPackageName();
@@ -141,14 +141,13 @@ public class CdmaSMSDispatcher extends SMSDispatcher {
@Override
public void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent,
PendingIntent deliveryIntent, Uri messageUri, String callingPkg,
- boolean persistMessage, int priority, boolean expectMore, int validityPeriod) {
+ boolean persistMessage) {
SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
- scAddr, destAddr, text, (deliveryIntent != null), null, priority);
+ scAddr, destAddr, text, (deliveryIntent != null), null);
if (pdu != null) {
HashMap map = getSmsTrackerMap(destAddr, scAddr, text, pdu);
SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
- messageUri, expectMore, text, true /*isText*/, persistMessage,
- priority, validityPeriod);
+ messageUri, false /*isExpectMore*/, text, true /*isText*/, persistMessage);
String carrierPackage = getCarrierAppPackageName();
if (carrierPackage != null) {
@@ -190,7 +189,7 @@ public class CdmaSMSDispatcher extends SMSDispatcher {
String message, SmsHeader smsHeader, int encoding,
PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart,
AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
- String fullMessageText, int priority, boolean expectMore, int validityPeriod) {
+ String fullMessageText) {
UserData uData = new UserData();
uData.payloadStr = message;
uData.userDataHeader = smsHeader;
@@ -206,14 +205,14 @@ public class CdmaSMSDispatcher extends SMSDispatcher {
* callback to the sender when that last fragment delivery
* has been acknowledged. */
SmsMessage.SubmitPdu submitPdu = SmsMessage.getSubmitPdu(destinationAddress,
- uData, (deliveryIntent != null) && lastPart, priority);
+ uData, (deliveryIntent != null) && lastPart);
HashMap map = getSmsTrackerMap(destinationAddress, scAddress,
message, submitPdu);
return getSmsTracker(map, sentIntent, deliveryIntent,
getFormat(), unsentPartCount, anyPartFailed, messageUri, smsHeader,
- (!lastPart || expectMore), fullMessageText, true /*isText*/,
- true /*persistMessage*/, priority, validityPeriod);
+ false /*isExpextMore*/, fullMessageText, true /*isText*/,
+ true /*persistMessage*/);
}
@Override
diff --git a/com/android/internal/telephony/cdma/SmsMessage.java b/com/android/internal/telephony/cdma/SmsMessage.java
index 14c5f4be..7a53ef63 100644
--- a/com/android/internal/telephony/cdma/SmsMessage.java
+++ b/com/android/internal/telephony/cdma/SmsMessage.java
@@ -99,15 +99,6 @@ public class SmsMessage extends SmsMessageBase {
private static final int RETURN_NO_ACK = 0;
private static final int RETURN_ACK = 1;
- /**
- * Supported priority modes for CDMA SMS messages
- * (See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1)
- */
- private static final int PRIORITY_NORMAL = 0x0;
- private static final int PRIORITY_INTERACTIVE = 0x1;
- private static final int PRIORITY_URGENT = 0x2;
- private static final int PRIORITY_EMERGENCY = 0x3;
-
private SmsEnvelope mEnvelope;
private BearerData mBearerData;
@@ -220,26 +211,6 @@ public class SmsMessage extends SmsMessageBase {
*/
public static SubmitPdu getSubmitPdu(String scAddr, String destAddr, String message,
boolean statusReportRequested, SmsHeader smsHeader) {
- return getSubmitPdu(scAddr, destAddr, message, statusReportRequested, smsHeader, -1);
- }
-
- /**
- * Get an SMS-SUBMIT PDU for a destination address and a message
- *
- * @param scAddr Service Centre address. Null means use default.
- * @param destAddr Address of the recipient.
- * @param message String representation of the message payload.
- * @param statusReportRequested Indicates whether a report is requested for this message.
- * @param smsHeader Array containing the data for the User Data Header, preceded
- * by the Element Identifiers.
- * @param priority Priority level of the message
- * @return a <code>SubmitPdu</code> containing the encoded SC
- * address, if applicable, and the encoded message.
- * Returns null on encode error.
- * @hide
- */
- public static SubmitPdu getSubmitPdu(String scAddr, String destAddr, String message,
- boolean statusReportRequested, SmsHeader smsHeader, int priority) {
/**
* TODO(cleanup): Do we really want silent failure like this?
@@ -253,7 +224,7 @@ public class SmsMessage extends SmsMessageBase {
UserData uData = new UserData();
uData.payloadStr = message;
uData.userDataHeader = smsHeader;
- return privateGetSubmitPdu(destAddr, statusReportRequested, uData, priority);
+ return privateGetSubmitPdu(destAddr, statusReportRequested, uData);
}
/**
@@ -311,22 +282,6 @@ public class SmsMessage extends SmsMessageBase {
}
/**
- * Get an SMS-SUBMIT PDU for a data message to a destination address &amp; port
- *
- * @param destAddr the address of the destination for the message
- * @param userData the data for the message
- * @param statusReportRequested Indicates whether a report is requested for this message.
- * @param priority Priority level of the message
- * @return a <code>SubmitPdu</code> containing the encoded SC
- * address, if applicable, and the encoded message.
- * Returns null on encode error.
- */
- public static SubmitPdu getSubmitPdu(String destAddr, UserData userData,
- boolean statusReportRequested, int priority) {
- return privateGetSubmitPdu(destAddr, statusReportRequested, userData, priority);
- }
-
- /**
* Note: This function is a GSM specific functionality which is not supported in CDMA mode.
*/
@Override
@@ -809,15 +764,6 @@ public class SmsMessage extends SmsMessageBase {
*/
private static SubmitPdu privateGetSubmitPdu(String destAddrStr, boolean statusReportRequested,
UserData userData) {
- return privateGetSubmitPdu(destAddrStr, statusReportRequested, userData, -1);
- }
-
- /**
- * Creates BearerData and Envelope from parameters for a Submit SMS.
- * @return byte stream for SubmitPdu.
- */
- private static SubmitPdu privateGetSubmitPdu(String destAddrStr, boolean statusReportRequested,
- UserData userData, int priority) {
/**
* TODO(cleanup): give this function a more meaningful name.
@@ -846,10 +792,6 @@ public class SmsMessage extends SmsMessageBase {
bearerData.userAckReq = false;
bearerData.readAckReq = false;
bearerData.reportReq = false;
- if (priority >= PRIORITY_NORMAL && priority <= PRIORITY_EMERGENCY) {
- bearerData.priorityIndicatorSet = true;
- bearerData.priority = priority;
- }
bearerData.userData = userData;
diff --git a/com/android/internal/telephony/dataconnection/DataCallResponse.java b/com/android/internal/telephony/dataconnection/DataCallResponse.java
deleted file mode 100644
index 5196bcee..00000000
--- a/com/android/internal/telephony/dataconnection/DataCallResponse.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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 com.android.internal.telephony.dataconnection;
-
-import android.os.SystemProperties;
-import android.text.TextUtils;
-
-/**
- * This is RIL_Data_Call_Response_v5 from ril.h
- */
-public class DataCallResponse {
- private final boolean DBG = true;
- private final String LOG_TAG = "DataCallResponse";
-
- public final int status;
- public final int suggestedRetryTime;
- public final int cid;
- public final int active;
- public final String type;
- public final String ifname;
- public final String [] addresses;
- public final String [] dnses;
- public final String[] gateways;
- public final String [] pcscf;
- public final int mtu;
-
- public DataCallResponse(int status, int suggestedRetryTime, int cid, int active, String type,
- String ifname, String addresses, String dnses, String gateways,
- String pcscf, int mtu) {
- this.status = status;
- this.suggestedRetryTime = suggestedRetryTime;
- this.cid = cid;
- this.active = active;
- this.type = (type == null) ? "" : type;
- this.ifname = (ifname == null) ? "" : ifname;
- if ((status == DcFailCause.NONE.getErrorCode()) && TextUtils.isEmpty(ifname)) {
- throw new RuntimeException("DataCallResponse, no ifname");
- }
- this.addresses = TextUtils.isEmpty(addresses) ? new String[0] : addresses.split(" ");
- this.dnses = TextUtils.isEmpty(dnses) ? new String[0] : dnses.split(" ");
-
- String[] myGateways = TextUtils.isEmpty(gateways)
- ? new String[0] : gateways.split(" ");
-
- // set gateways
- if (myGateways.length == 0) {
- String propertyPrefix = "net." + this.ifname + ".";
- String sysGateways = SystemProperties.get(propertyPrefix + "gw");
- if (sysGateways != null) {
- myGateways = sysGateways.split(" ");
- } else {
- myGateways = new String[0];
- }
- }
- this.gateways = myGateways;
-
- this.pcscf = TextUtils.isEmpty(pcscf) ? new String[0] : pcscf.split(" ");
- this.mtu = mtu;
- }
-
- @Override
- public String toString() {
- StringBuffer sb = new StringBuffer();
- sb.append("DataCallResponse: {")
- .append(" status=").append(status)
- .append(" retry=").append(suggestedRetryTime)
- .append(" cid=").append(cid)
- .append(" active=").append(active)
- .append(" type=").append(type)
- .append(" ifname=").append(ifname)
- .append(" mtu=").append(mtu)
- .append(" addresses=[");
- for (String addr : addresses) {
- sb.append(addr);
- sb.append(",");
- }
- if (addresses.length > 0) sb.deleteCharAt(sb.length()-1);
- sb.append("] dnses=[");
- for (String addr : dnses) {
- sb.append(addr);
- sb.append(",");
- }
- if (dnses.length > 0) sb.deleteCharAt(sb.length()-1);
- sb.append("] gateways=[");
- for (String addr : gateways) {
- sb.append(addr);
- sb.append(",");
- }
- if (gateways.length > 0) sb.deleteCharAt(sb.length()-1);
- sb.append("] pcscf=[");
- for (String addr : pcscf) {
- sb.append(addr);
- sb.append(",");
- }
- if (pcscf.length > 0) sb.deleteCharAt(sb.length()-1);
- sb.append("]}");
- return sb.toString();
- }
-}
diff --git a/com/android/internal/telephony/dataconnection/DataConnection.java b/com/android/internal/telephony/dataconnection/DataConnection.java
index 3dbcc3d5..d30301eb 100644
--- a/com/android/internal/telephony/dataconnection/DataConnection.java
+++ b/com/android/internal/telephony/dataconnection/DataConnection.java
@@ -37,7 +37,9 @@ import android.os.SystemProperties;
import android.telephony.Rlog;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
+import android.telephony.data.DataCallResponse;
import android.telephony.data.DataProfile;
+import android.telephony.data.InterfaceAddress;
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Pair;
@@ -452,13 +454,13 @@ public class DataConnection extends StateMachine {
addState(mDisconnectingErrorCreatingConnection, mDefaultState);
setInitialState(mInactiveState);
- mApnContexts = new HashMap<ApnContext, ConnectionParams>();
+ mApnContexts = new HashMap<>();
}
/**
* Begin setting up a data connection, calls setupDataCall
* and the ConnectionParams will be returned with the
- * EVENT_SETUP_DATA_CONNECTION_DONE AsyncResul.userObj.
+ * EVENT_SETUP_DATA_CONNECTION_DONE AsyncResult.userObj.
*
* @param cp is the connection parameters
*/
@@ -473,7 +475,7 @@ public class DataConnection extends StateMachine {
DataCallResponse response = new DataCallResponse(
mDcTesterFailBringUpAll.getDcFailBringUp().mFailCause.getErrorCode(),
mDcTesterFailBringUpAll.getDcFailBringUp().mSuggestedRetryTime, 0, 0, "", "",
- "", "", "", "", PhoneConstants.UNSET_MTU);
+ null, null, null, null, PhoneConstants.UNSET_MTU);
Message msg = obtainMessage(EVENT_SETUP_DATA_CONNECTION_DONE, cp);
AsyncResult.forMessage(msg, response, null);
@@ -717,16 +719,16 @@ public class DataConnection extends StateMachine {
result.mFailCause = DcFailCause.RADIO_NOT_AVAILABLE;
} else {
result = SetupResult.ERR_RilError;
- result.mFailCause = DcFailCause.fromInt(response.status);
+ result.mFailCause = DcFailCause.fromInt(response.getStatus());
}
- } else if (response.status != 0) {
+ } else if (response.getStatus() != 0) {
result = SetupResult.ERR_RilError;
- result.mFailCause = DcFailCause.fromInt(response.status);
+ result.mFailCause = DcFailCause.fromInt(response.getStatus());
} else {
if (DBG) log("onSetupConnectionCompleted received successful DataCallResponse");
- mCid = response.cid;
+ mCid = response.getCallId();
- mPcscfAddr = response.pcscf;
+ mPcscfAddr = response.getPcscfs().toArray(new String[response.getPcscfs().size()]);
result = updateLinkProperty(response).setupResult;
}
@@ -1027,7 +1029,7 @@ public class DataConnection extends StateMachine {
private SetupResult setLinkProperties(DataCallResponse response,
LinkProperties linkProperties) {
// Check if system property dns usable
- String propertyPrefix = "net." + response.ifname + ".";
+ String propertyPrefix = "net." + response.getIfname() + ".";
String dnsServers[] = new String[2];
dnsServers[0] = SystemProperties.get(propertyPrefix + "dns1");
dnsServers[1] = SystemProperties.get(propertyPrefix + "dns2");
@@ -1039,70 +1041,45 @@ public class DataConnection extends StateMachine {
// a failure we'll clear again at the bottom of this code.
linkProperties.clear();
- if (response.status == DcFailCause.NONE.getErrorCode()) {
+ if (response.getStatus() == DcFailCause.NONE.getErrorCode()) {
try {
// set interface name
- linkProperties.setInterfaceName(response.ifname);
+ linkProperties.setInterfaceName(response.getIfname());
// set link addresses
- if (response.addresses != null && response.addresses.length > 0) {
- for (String addr : response.addresses) {
- addr = addr.trim();
- if (addr.isEmpty()) continue;
- LinkAddress la;
- int addrPrefixLen;
-
- String [] ap = addr.split("/");
- if (ap.length == 2) {
- addr = ap[0];
- addrPrefixLen = Integer.parseInt(ap[1]);
- } else {
- addrPrefixLen = 0;
- }
- InetAddress ia;
- try {
- ia = NetworkUtils.numericToInetAddress(addr);
- } catch (IllegalArgumentException e) {
- throw new UnknownHostException("Non-numeric ip addr=" + addr);
- }
- if (!ia.isAnyLocalAddress()) {
+ if (response.getAddresses().size() > 0) {
+ for (InterfaceAddress ia : response.getAddresses()) {
+ if (!ia.getAddress().isAnyLocalAddress()) {
+ int addrPrefixLen = ia.getNetworkPrefixLength();
if (addrPrefixLen == 0) {
// Assume point to point
- addrPrefixLen = (ia instanceof Inet4Address) ? 32 : 128;
+ addrPrefixLen =
+ (ia.getAddress() instanceof Inet4Address) ? 32 : 128;
}
- if (DBG) log("addr/pl=" + addr + "/" + addrPrefixLen);
+ if (DBG) log("addr/pl=" + ia.getAddress() + "/" + addrPrefixLen);
+ LinkAddress la;
try {
- la = new LinkAddress(ia, addrPrefixLen);
+ la = new LinkAddress(ia.getAddress(), addrPrefixLen);
} catch (IllegalArgumentException e) {
throw new UnknownHostException("Bad parameter for LinkAddress, ia="
- + ia.getHostAddress() + "/" + addrPrefixLen);
+ + ia.getAddress().getHostAddress() + "/" + addrPrefixLen);
}
linkProperties.addLinkAddress(la);
}
}
} else {
- throw new UnknownHostException("no address for ifname=" + response.ifname);
+ throw new UnknownHostException("no address for ifname=" + response.getIfname());
}
// set dns servers
- if (response.dnses != null && response.dnses.length > 0) {
- for (String addr : response.dnses) {
- addr = addr.trim();
- if (addr.isEmpty()) continue;
- InetAddress ia;
- try {
- ia = NetworkUtils.numericToInetAddress(addr);
- } catch (IllegalArgumentException e) {
- throw new UnknownHostException("Non-numeric dns addr=" + addr);
- }
- if (!ia.isAnyLocalAddress()) {
- linkProperties.addDnsServer(ia);
+ if (response.getDnses().size() > 0) {
+ for (InetAddress dns : response.getDnses()) {
+ if (!dns.isAnyLocalAddress()) {
+ linkProperties.addDnsServer(dns);
}
}
} else if (okToUseSystemPropertyDns) {
- dnsServers[0] = SystemProperties.get(propertyPrefix + "dns1");
- dnsServers[1] = SystemProperties.get(propertyPrefix + "dns2");
for (String dnsAddr : dnsServers) {
dnsAddr = dnsAddr.trim();
if (dnsAddr.isEmpty()) continue;
@@ -1120,27 +1097,19 @@ public class DataConnection extends StateMachine {
throw new UnknownHostException("Empty dns response and no system default dns");
}
- for (String addr : response.gateways) {
- addr = addr.trim();
- if (addr.isEmpty()) continue;
- InetAddress ia;
- try {
- ia = NetworkUtils.numericToInetAddress(addr);
- } catch (IllegalArgumentException e) {
- throw new UnknownHostException("Non-numeric gateway addr=" + addr);
- }
- // Allow 0.0.0.0 or :: as a gateway; this indicates a point-to-point interface.
- linkProperties.addRoute(new RouteInfo(ia));
+ for (InetAddress gateway : response.getGateways()) {
+ // Allow 0.0.0.0 or :: as a gateway;
+ // this indicates a point-to-point interface.
+ linkProperties.addRoute(new RouteInfo(gateway));
}
// set interface MTU
// this may clobber the setting read from the APN db, but that's ok
- linkProperties.setMtu(response.mtu);
+ linkProperties.setMtu(response.getMtu());
result = SetupResult.SUCCESS;
} catch (UnknownHostException e) {
log("setLinkProperties: UnknownHostException " + e);
- e.printStackTrace();
result = SetupResult.ERR_UnacceptableParameter;
}
} else {
@@ -1150,8 +1119,8 @@ public class DataConnection extends StateMachine {
// An error occurred so clear properties
if (result != SetupResult.SUCCESS) {
if (DBG) {
- log("setLinkProperties: error clearing LinkProperties status=" + response.status
- + " result=" + result);
+ log("setLinkProperties: error clearing LinkProperties status="
+ + response.getStatus() + " result=" + result);
}
linkProperties.clear();
}
@@ -2070,19 +2039,19 @@ public class DataConnection extends StateMachine {
*/
// The value < 0 means no value is suggested
- if (response.suggestedRetryTime < 0) {
+ if (response.getSuggestedRetryTime() < 0) {
if (DBG) log("No suggested retry delay.");
return RetryManager.NO_SUGGESTED_RETRY_DELAY;
}
// The value of Integer.MAX_VALUE(0x7fffffff) means no retry.
- else if (response.suggestedRetryTime == Integer.MAX_VALUE) {
+ else if (response.getSuggestedRetryTime() == Integer.MAX_VALUE) {
if (DBG) log("Modem suggested not retrying.");
return RetryManager.NO_RETRY;
}
// We need to cast it to long because the value returned from RIL is a 32-bit integer,
// but the time values used in AlarmManager are all 64-bit long.
- return (long) response.suggestedRetryTime;
+ return (long) response.getSuggestedRetryTime();
}
/**
diff --git a/com/android/internal/telephony/dataconnection/DcController.java b/com/android/internal/telephony/dataconnection/DcController.java
index 291d6f5e..c21713f1 100644
--- a/com/android/internal/telephony/dataconnection/DcController.java
+++ b/com/android/internal/telephony/dataconnection/DcController.java
@@ -27,6 +27,7 @@ import android.os.Message;
import android.telephony.PhoneStateListener;
import android.telephony.Rlog;
import android.telephony.TelephonyManager;
+import android.telephony.data.DataCallResponse;
import com.android.internal.telephony.DctConstants;
import com.android.internal.telephony.Phone;
@@ -225,7 +226,7 @@ public class DcController extends StateMachine {
HashMap<Integer, DataCallResponse> dataCallResponseListByCid =
new HashMap<Integer, DataCallResponse>();
for (DataCallResponse dcs : dcsList) {
- dataCallResponseListByCid.put(dcs.cid, dcs);
+ dataCallResponseListByCid.put(dcs.getCallId(), dcs);
}
// Add a DC that is active but not in the
@@ -248,7 +249,7 @@ public class DcController extends StateMachine {
for (DataCallResponse newState : dcsList) {
- DataConnection dc = mDcListActiveByCid.get(newState.cid);
+ DataConnection dc = mDcListActiveByCid.get(newState.getCallId());
if (dc == null) {
// UNSOL_DATA_CALL_LIST_CHANGED arrived before SETUP_DATA_CALL completed.
loge("onDataStateChanged: no associated DC yet, ignore");
@@ -260,14 +261,16 @@ public class DcController extends StateMachine {
} else {
// Determine if the connection/apnContext should be cleaned up
// or just a notification should be sent out.
- if (DBG) log("onDataStateChanged: Found ConnId=" + newState.cid
- + " newState=" + newState.toString());
- if (newState.active == DATA_CONNECTION_ACTIVE_PH_LINK_INACTIVE) {
+ if (DBG) {
+ log("onDataStateChanged: Found ConnId=" + newState.getCallId()
+ + " newState=" + newState.toString());
+ }
+ if (newState.getActive() == DATA_CONNECTION_ACTIVE_PH_LINK_INACTIVE) {
if (mDct.isCleanupRequired.get()) {
apnsToCleanup.addAll(dc.mApnContexts.keySet());
mDct.isCleanupRequired.set(false);
} else {
- DcFailCause failCause = DcFailCause.fromInt(newState.status);
+ DcFailCause failCause = DcFailCause.fromInt(newState.getStatus());
if (failCause.isRestartRadioFail(mPhone.getContext(),
mPhone.getSubId())) {
if (DBG) {
@@ -352,10 +355,10 @@ public class DcController extends StateMachine {
}
}
- if (newState.active == DATA_CONNECTION_ACTIVE_PH_LINK_UP) {
+ if (newState.getActive() == DATA_CONNECTION_ACTIVE_PH_LINK_UP) {
isAnyDataCallActive = true;
}
- if (newState.active == DATA_CONNECTION_ACTIVE_PH_LINK_DORMANT) {
+ if (newState.getActive() == DATA_CONNECTION_ACTIVE_PH_LINK_DORMANT) {
isAnyDataCallDormant = true;
}
}
diff --git a/com/android/internal/telephony/dataconnection/DcTracker.java b/com/android/internal/telephony/dataconnection/DcTracker.java
index 540ec544..86ee9a2d 100644
--- a/com/android/internal/telephony/dataconnection/DcTracker.java
+++ b/com/android/internal/telephony/dataconnection/DcTracker.java
@@ -793,7 +793,7 @@ public class DcTracker extends Handler {
/**
* Modify {@link android.provider.Settings.Global#MOBILE_DATA} value.
*/
- public void setDataEnabled(boolean enable) {
+ public void setUserDataEnabled(boolean enable) {
Message msg = obtainMessage(DctConstants.CMD_SET_USER_DATA_ENABLE);
msg.arg1 = enable ? 1 : 0;
if (DBG) log("setDataEnabled: sendMessage: enable=" + enable);
@@ -1177,6 +1177,10 @@ public class DcTracker extends Handler {
}
}
+ /**
+ * Whether data is enabled. This does not only check isUserDataEnabled(), but also
+ * others like CarrierDataEnabled and internalDataEnabled.
+ */
@VisibleForTesting
public boolean isDataEnabled() {
return mDataEnabledSettings.isDataEnabled();
@@ -2574,9 +2578,11 @@ public class DcTracker extends Handler {
}
/**
- * Return current {@link android.provider.Settings.Global#MOBILE_DATA} value.
+ * Whether data is enabled by user. Unlike isDataEnabled, this only
+ * checks user setting stored in {@link android.provider.Settings.Global#MOBILE_DATA}
+ * if not provisioning, or isProvisioningDataEnabled if provisioning.
*/
- public boolean getDataEnabled() {
+ public boolean isUserDataEnabled() {
if (mDataEnabledSettings.isProvisioning()) {
return mDataEnabledSettings.isProvisioningDataEnabled();
} else {
@@ -4254,8 +4260,10 @@ public class DcTracker extends Handler {
if (dcac != null) {
result = dcac.getPcscfAddr();
- for (int i = 0; i < result.length; i++) {
- log("Pcscf[" + i + "]: " + result[i]);
+ if (result != null) {
+ for (int i = 0; i < result.length; i++) {
+ log("Pcscf[" + i + "]: " + result[i]);
+ }
}
return result;
}
diff --git a/com/android/internal/telephony/euicc/EuiccConnector.java b/com/android/internal/telephony/euicc/EuiccConnector.java
index 5e07eed8..3ee21641 100644
--- a/com/android/internal/telephony/euicc/EuiccConnector.java
+++ b/com/android/internal/telephony/euicc/EuiccConnector.java
@@ -46,12 +46,14 @@ import android.service.euicc.IGetDownloadableSubscriptionMetadataCallback;
import android.service.euicc.IGetEidCallback;
import android.service.euicc.IGetEuiccInfoCallback;
import android.service.euicc.IGetEuiccProfileInfoListCallback;
+import android.service.euicc.IGetOtaStatusCallback;
import android.service.euicc.IRetainSubscriptionsForFactoryResetCallback;
import android.service.euicc.ISwitchToSubscriptionCallback;
import android.service.euicc.IUpdateSubscriptionNicknameCallback;
import android.telephony.SubscriptionManager;
import android.telephony.euicc.DownloadableSubscription;
import android.telephony.euicc.EuiccInfo;
+import android.telephony.euicc.EuiccManager.OtaStatus;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
@@ -132,6 +134,7 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
private static final int CMD_UPDATE_SUBSCRIPTION_NICKNAME = 108;
private static final int CMD_ERASE_SUBSCRIPTIONS = 109;
private static final int CMD_RETAIN_SUBSCRIPTIONS = 110;
+ private static final int CMD_GET_OTA_STATUS = 111;
private static boolean isEuiccCommand(int what) {
return what >= CMD_GET_EID;
@@ -185,6 +188,13 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
void onGetEidComplete(String eid);
}
+ /** Callback class for {@link #getOtaStatus}. */
+ @VisibleForTesting(visibility = PACKAGE)
+ public interface GetOtaStatusCommandCallback extends BaseEuiccCommandCallback {
+ /** Called when the getting OTA status lookup has completed. */
+ void onGetOtaStatusComplete(@OtaStatus int status);
+ }
+
static class GetMetadataRequest {
DownloadableSubscription mSubscription;
boolean mForceDeactivateSim;
@@ -373,6 +383,12 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
sendMessage(CMD_GET_EID, callback);
}
+ /** Asynchronously get OTA status. */
+ @VisibleForTesting(visibility = PACKAGE)
+ public void getOtaStatus(GetOtaStatusCommandCallback callback) {
+ sendMessage(CMD_GET_OTA_STATUS, callback);
+ }
+
/** Asynchronously fetch metadata for the given downloadable subscription. */
@VisibleForTesting(visibility = PACKAGE)
public void getDownloadableSubscriptionMetadata(DownloadableSubscription subscription,
@@ -809,6 +825,20 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
});
break;
}
+ case CMD_GET_OTA_STATUS: {
+ mEuiccService.getOtaStatus(slotId,
+ new IGetOtaStatusCallback.Stub() {
+ @Override
+ public void onSuccess(@OtaStatus int status) {
+ sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+ ((GetOtaStatusCommandCallback) callback)
+ .onGetOtaStatusComplete(status);
+ onCommandEnd(callback);
+ });
+ }
+ });
+ break;
+ }
default: {
Log.wtf(TAG, "Unimplemented eUICC command: " + message.what);
callback.onEuiccServiceUnavailable();
@@ -851,6 +881,7 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
case CMD_GET_EUICC_INFO:
case CMD_ERASE_SUBSCRIPTIONS:
case CMD_RETAIN_SUBSCRIPTIONS:
+ case CMD_GET_OTA_STATUS:
return (BaseEuiccCommandCallback) message.obj;
case CMD_GET_DOWNLOADABLE_SUBSCRIPTION_METADATA:
return ((GetMetadataRequest) message.obj).mCallback;
diff --git a/com/android/internal/telephony/euicc/EuiccController.java b/com/android/internal/telephony/euicc/EuiccController.java
index dc847187..78ff3560 100644
--- a/com/android/internal/telephony/euicc/EuiccController.java
+++ b/com/android/internal/telephony/euicc/EuiccController.java
@@ -15,6 +15,8 @@
*/
package com.android.internal.telephony.euicc;
+import static android.telephony.euicc.EuiccManager.EUICC_OTA_STATUS_UNAVAILABLE;
+
import android.Manifest;
import android.annotation.Nullable;
import android.app.AppOpsManager;
@@ -38,6 +40,7 @@ import android.telephony.UiccAccessRule;
import android.telephony.euicc.DownloadableSubscription;
import android.telephony.euicc.EuiccInfo;
import android.telephony.euicc.EuiccManager;
+import android.telephony.euicc.EuiccManager.OtaStatus;
import android.text.TextUtils;
import android.util.Log;
@@ -169,6 +172,25 @@ public class EuiccController extends IEuiccController.Stub {
}
}
+ /**
+ * Return the current status of OTA update.
+ *
+ * <p>For API simplicity, this call blocks until completion; while it requires an IPC to load,
+ * that IPC should generally be fast.
+ */
+ @Override
+ public @OtaStatus int getOtaStatus() {
+ if (!callerCanWriteEmbeddedSubscriptions()) {
+ throw new SecurityException("Must have WRITE_EMBEDDED_SUBSCRIPTIONS to get OTA status");
+ }
+ long token = Binder.clearCallingIdentity();
+ try {
+ return blockingGetOtaStatusFromEuiccService();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
@Override
public void getDownloadableSubscriptionMetadata(DownloadableSubscription subscription,
String callingPackage, PendingIntent callbackIntent) {
@@ -953,6 +975,25 @@ public class EuiccController extends IEuiccController.Stub {
return awaitResult(latch, eidRef);
}
+ private @OtaStatus int blockingGetOtaStatusFromEuiccService() {
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicReference<Integer> statusRef =
+ new AtomicReference<>(EUICC_OTA_STATUS_UNAVAILABLE);
+ mConnector.getOtaStatus(new EuiccConnector.GetOtaStatusCommandCallback() {
+ @Override
+ public void onGetOtaStatusComplete(@OtaStatus int status) {
+ statusRef.set(status);
+ latch.countDown();
+ }
+
+ @Override
+ public void onEuiccServiceUnavailable() {
+ latch.countDown();
+ }
+ });
+ return awaitResult(latch, statusRef);
+ }
+
@Nullable
private EuiccInfo blockingGetEuiccInfoFromEuiccService() {
CountDownLatch latch = new CountDownLatch(1);
diff --git a/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java b/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java
index dec98057..f30b3862 100644
--- a/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java
+++ b/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java
@@ -78,6 +78,7 @@ public class GsmCellBroadcastHandler extends CellBroadcastHandler {
handleBroadcastSms(cbMessage);
return true;
}
+ if (VDBG) log("Not handled GSM broadcasts.");
}
return super.handleSmsMessage(message);
}
@@ -106,6 +107,7 @@ public class GsmCellBroadcastHandler extends CellBroadcastHandler {
}
SmsCbHeader header = new SmsCbHeader(receivedPdu);
+ if (VDBG) log("header=" + header);
String plmn = TelephonyManager.from(mContext).getNetworkOperatorForPhone(
mPhone.getPhoneId());
int lac = -1;
@@ -154,12 +156,14 @@ public class GsmCellBroadcastHandler extends CellBroadcastHandler {
mSmsCbPageMap.put(concatInfo, pdus);
}
+ if (VDBG) log("pdus size=" + pdus.length);
// Page parameter is one-based
pdus[header.getPageIndex() - 1] = receivedPdu;
for (byte[] pdu : pdus) {
if (pdu == null) {
// Still missing pages, exit
+ log("still missing pdu");
return null;
}
}
diff --git a/com/android/internal/telephony/gsm/GsmMmiCode.java b/com/android/internal/telephony/gsm/GsmMmiCode.java
index 3376e2b2..d07fb123 100644
--- a/com/android/internal/telephony/gsm/GsmMmiCode.java
+++ b/com/android/internal/telephony/gsm/GsmMmiCode.java
@@ -1252,6 +1252,9 @@ public final class GsmMmiCode extends Handler implements MmiCode {
} else if (err == CommandException.Error.SS_MODIFIED_TO_SS) {
Rlog.i(LOG_TAG, "SS_MODIFIED_TO_SS");
return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_ss);
+ } else if (err == CommandException.Error.OEM_ERROR_1) {
+ Rlog.i(LOG_TAG, "OEM_ERROR_1 USSD_MODIFIED_TO_DIAL_VIDEO");
+ return mContext.getText(com.android.internal.R.string.stk_cc_ussd_to_dial_video);
}
}
diff --git a/com/android/internal/telephony/gsm/GsmSMSDispatcher.java b/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
index 6e4a79f8..8f18c61d 100644
--- a/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
+++ b/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
@@ -157,7 +157,7 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
if (pdu != null) {
HashMap map = getSmsTrackerMap(destAddr, scAddr, destPort, data, pdu);
SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
- null /*messageUri*/, false /*expectMore*/, null /*fullMessageText*/,
+ null /*messageUri*/, false /*isExpectMore*/, null /*fullMessageText*/,
false /*isText*/, true /*persistMessage*/);
String carrierPackage = getCarrierAppPackageName();
@@ -179,14 +179,14 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
@Override
public void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent,
PendingIntent deliveryIntent, Uri messageUri, String callingPkg,
- boolean persistMessage, int priority, boolean expectMore, int validityPeriod) {
+ boolean persistMessage) {
SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
- scAddr, destAddr, text, (deliveryIntent != null), validityPeriod);
+ scAddr, destAddr, text, (deliveryIntent != null));
if (pdu != null) {
HashMap map = getSmsTrackerMap(destAddr, scAddr, text, pdu);
SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
- messageUri, false /*expectMore*/, text /*fullMessageText*/, true /*isText*/,
- persistMessage, priority, validityPeriod);
+ messageUri, false /*isExpectMore*/, text /*fullMessageText*/, true /*isText*/,
+ persistMessage);
String carrierPackage = getCarrierAppPackageName();
if (carrierPackage != null) {
@@ -221,17 +221,17 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
String message, SmsHeader smsHeader, int encoding,
PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart,
AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
- String fullMessageText, int priority, boolean expectMore, int validityPeriod) {
+ String fullMessageText) {
SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(scAddress, destinationAddress,
message, deliveryIntent != null, SmsHeader.toByteArray(smsHeader),
- encoding, smsHeader.languageTable, smsHeader.languageShiftTable, validityPeriod);
+ encoding, smsHeader.languageTable, smsHeader.languageShiftTable);
if (pdu != null) {
HashMap map = getSmsTrackerMap(destinationAddress, scAddress,
message, pdu);
return getSmsTracker(map, sentIntent,
deliveryIntent, getFormat(), unsentPartCount, anyPartFailed, messageUri,
- smsHeader, (!lastPart || expectMore), fullMessageText, true /*isText*/,
- false /*persistMessage*/, priority, validityPeriod);
+ smsHeader, !lastPart, fullMessageText, true /*isText*/,
+ false /*persistMessage*/);
} else {
Rlog.e(TAG, "GsmSMSDispatcher.sendNewSubmitPdu(): getSubmitPdu() returned null");
return null;
diff --git a/com/android/internal/telephony/gsm/SmsCbHeader.java b/com/android/internal/telephony/gsm/SmsCbHeader.java
index d267ad20..0dbc186e 100644
--- a/com/android/internal/telephony/gsm/SmsCbHeader.java
+++ b/com/android/internal/telephony/gsm/SmsCbHeader.java
@@ -441,10 +441,11 @@ public class SmsCbHeader {
@Override
public String toString() {
- return "SmsCbHeader{GS=" + mGeographicalScope + ", serialNumber=0x" +
- Integer.toHexString(mSerialNumber) +
- ", messageIdentifier=0x" + Integer.toHexString(mMessageIdentifier) +
- ", DCS=0x" + Integer.toHexString(mDataCodingScheme) +
- ", page " + mPageIndex + " of " + mNrOfPages + '}';
+ return "SmsCbHeader{GS=" + mGeographicalScope + ", serialNumber=0x"
+ + Integer.toHexString(mSerialNumber)
+ + ", messageIdentifier=0x" + Integer.toHexString(mMessageIdentifier)
+ + ", format=" + mFormat
+ + ", DCS=0x" + Integer.toHexString(mDataCodingScheme)
+ + ", page " + mPageIndex + " of " + mNrOfPages + '}';
}
} \ No newline at end of file
diff --git a/com/android/internal/telephony/gsm/SmsMessage.java b/com/android/internal/telephony/gsm/SmsMessage.java
index 4f5bfa91..1ca19e01 100644
--- a/com/android/internal/telephony/gsm/SmsMessage.java
+++ b/com/android/internal/telephony/gsm/SmsMessage.java
@@ -89,18 +89,6 @@ public class SmsMessage extends SmsMessageBase {
private int mVoiceMailCount = 0;
- private static final int VALIDITY_PERIOD_FORMAT_NONE = 0x00;
- private static final int VALIDITY_PERIOD_FORMAT_ENHANCED = 0x01;
- private static final int VALIDITY_PERIOD_FORMAT_RELATIVE = 0x02;
- private static final int VALIDITY_PERIOD_FORMAT_ABSOLUTE = 0x03;
-
- //Validity Period min - 5 mins
- private static final int VALIDITY_PERIOD_MIN = 5;
- //Validity Period max - 63 weeks
- private static final int VALIDITY_PERIOD_MAX = 635040;
-
- private static final int INVALID_VALIDITY_PERIOD = -1;
-
public static class SubmitPdu extends SubmitPduBase {
}
@@ -214,45 +202,6 @@ public class SmsMessage extends SmsMessageBase {
}
/**
- * Get Encoded Relative Validty Period Value from Validity period in mins.
- *
- * @param validityPeriod Validity period in mins.
- *
- * Refer specification 3GPP TS 23.040 V6.8.1 section 9.2.3.12.1.
- * ||relValidityPeriod (TP-VP) || || validityPeriod ||
- *
- * 0 to 143 ---> (TP-VP + 1) x 5 minutes
- *
- * 144 to 167 ---> 12 hours + ((TP-VP -143) x 30 minutes)
- *
- * 168 to 196 ---> (TP-VP - 166) x 1 day
- *
- * 197 to 255 ---> (TP-VP - 192) x 1 week
- *
- * @return relValidityPeriod Encoded Relative Validity Period Value.
- * @hide
- */
- public static int getRelativeValidityPeriod(int validityPeriod) {
- int relValidityPeriod = INVALID_VALIDITY_PERIOD;
-
- if (validityPeriod < VALIDITY_PERIOD_MIN || validityPeriod > VALIDITY_PERIOD_MAX) {
- Rlog.e(LOG_TAG,"Invalid Validity Period" + validityPeriod);
- return relValidityPeriod;
- }
-
- if (validityPeriod <= 720) {
- relValidityPeriod = (validityPeriod / 5) - 1;
- } else if (validityPeriod <= 1440) {
- relValidityPeriod = ((validityPeriod - 720) / 30) + 143;
- } else if (validityPeriod <= 43200) {
- relValidityPeriod = (validityPeriod / 1440) + 166;
- } else if (validityPeriod <= 635040) {
- relValidityPeriod = (validityPeriod / 10080) + 192;
- }
- return relValidityPeriod;
- }
-
- /**
* Get an SMS-SUBMIT PDU for a destination address and a message
*
* @param scAddress Service Centre address. Null means use default.
@@ -287,29 +236,6 @@ public class SmsMessage extends SmsMessageBase {
String destinationAddress, String message,
boolean statusReportRequested, byte[] header, int encoding,
int languageTable, int languageShiftTable) {
- return getSubmitPdu(scAddress, destinationAddress, message, statusReportRequested,
- header, encoding, languageTable, languageShiftTable, -1);
- }
-
- /**
- * Get an SMS-SUBMIT PDU for a destination address and a message using the
- * specified encoding.
- *
- * @param scAddress Service Centre address. Null means use default.
- * @param encoding Encoding defined by constants in
- * com.android.internal.telephony.SmsConstants.ENCODING_*
- * @param languageTable
- * @param languageShiftTable
- * @param validityPeriod Validity Period of the message in Minutes.
- * @return a <code>SubmitPdu</code> containing the encoded SC
- * address, if applicable, and the encoded message.
- * Returns null on encode error.
- * @hide
- */
- public static SubmitPdu getSubmitPdu(String scAddress,
- String destinationAddress, String message,
- boolean statusReportRequested, byte[] header, int encoding,
- int languageTable, int languageShiftTable, int validityPeriod) {
// Perform null parameter checks.
if (message == null || destinationAddress == null) {
@@ -346,19 +272,8 @@ public class SmsMessage extends SmsMessageBase {
}
SubmitPdu ret = new SubmitPdu();
-
- int validityPeriodFormat = VALIDITY_PERIOD_FORMAT_NONE;
- int relativeValidityPeriod = INVALID_VALIDITY_PERIOD;
-
- // TP-Validity-Period-Format (TP-VPF) in 3GPP TS 23.040 V6.8.1 section 9.2.3.3
- //bit 4:3 = 10 - TP-VP field present - relative format
- if((relativeValidityPeriod = getRelativeValidityPeriod(validityPeriod)) >= 0) {
- validityPeriodFormat = VALIDITY_PERIOD_FORMAT_RELATIVE;
- }
-
- byte mtiByte = (byte)(0x01 | (validityPeriodFormat << 0x03) |
- (header != null ? 0x40 : 0x00));
-
+ // MTI = SMS-SUBMIT, UDHI = header != null
+ byte mtiByte = (byte)(0x01 | (header != null ? 0x40 : 0x00));
ByteArrayOutputStream bo = getSubmitPduHead(
scAddress, destinationAddress, mtiByte,
statusReportRequested, ret);
@@ -423,11 +338,7 @@ public class SmsMessage extends SmsMessageBase {
bo.write(0x08);
}
- if (validityPeriodFormat == VALIDITY_PERIOD_FORMAT_RELATIVE) {
- // ( TP-Validity-Period - relative format)
- bo.write(relativeValidityPeriod);
- }
-
+ // (no TP-Validity-Period)
bo.write(userData, 0, userData.length);
ret.encodedMessage = bo.toByteArray();
return ret;
@@ -477,24 +388,6 @@ public class SmsMessage extends SmsMessageBase {
}
/**
- * Get an SMS-SUBMIT PDU for a destination address and a message
- *
- * @param scAddress Service Centre address. Null means use default.
- * @param destinationAddress the address of the destination for the message
- * @param statusReportRequested staus report of the message Requested
- * @param validityPeriod Validity Period of the message in Minutes.
- * @return a <code>SubmitPdu</code> containing the encoded SC
- * address, if applicable, and the encoded message.
- * Returns null on encode error.
- */
- public static SubmitPdu getSubmitPdu(String scAddress,
- String destinationAddress, String message,
- boolean statusReportRequested, int validityPeriod) {
- return getSubmitPdu(scAddress, destinationAddress, message, statusReportRequested,
- null, ENCODING_UNKNOWN, 0, 0, validityPeriod);
- }
-
- /**
* Get an SMS-SUBMIT PDU for a data message to a destination address &amp; port
*
* @param scAddress Service Centre address. null == use default
diff --git a/com/android/internal/telephony/imsphone/ImsPhone.java b/com/android/internal/telephony/imsphone/ImsPhone.java
index e54d7bcc..c2f1f232 100644
--- a/com/android/internal/telephony/imsphone/ImsPhone.java
+++ b/com/android/internal/telephony/imsphone/ImsPhone.java
@@ -953,22 +953,37 @@ public class ImsPhone extends ImsPhoneBase {
}
public void getCallBarring(String facility, Message onComplete) {
- if (DBG) Rlog.d(LOG_TAG, "getCallBarring facility=" + facility);
+ getCallBarring(facility, onComplete, CommandsInterface.SERVICE_CLASS_NONE);
+ }
+
+ public void getCallBarring(String facility, Message onComplete, int serviceClass) {
+ if (DBG) {
+ Rlog.d(LOG_TAG, "getCallBarring facility=" + facility
+ + ", serviceClass = " + serviceClass);
+ }
Message resp;
resp = obtainMessage(EVENT_GET_CALL_BARRING_DONE, onComplete);
try {
ImsUtInterface ut = mCT.getUtInterface();
- ut.queryCallBarring(getCBTypeFromFacility(facility), resp);
+ ut.queryCallBarring(getCBTypeFromFacility(facility), resp, serviceClass);
} catch (ImsException e) {
sendErrorResponse(onComplete, e);
}
}
- public void setCallBarring(String facility, boolean lockState, String password, Message
- onComplete) {
- if (DBG) Rlog.d(LOG_TAG, "setCallBarring facility=" + facility
- + ", lockState=" + lockState);
+ public void setCallBarring(String facility, boolean lockState, String password,
+ Message onComplete) {
+ setCallBarring(facility, lockState, password, onComplete,
+ CommandsInterface.SERVICE_CLASS_NONE);
+ }
+
+ public void setCallBarring(String facility, boolean lockState, String password,
+ Message onComplete, int serviceClass) {
+ if (DBG) {
+ Rlog.d(LOG_TAG, "setCallBarring facility=" + facility
+ + ", lockState=" + lockState + ", serviceClass = " + serviceClass);
+ }
Message resp;
resp = obtainMessage(EVENT_SET_CALL_BARRING_DONE, onComplete);
@@ -983,7 +998,8 @@ public class ImsPhone extends ImsPhoneBase {
try {
ImsUtInterface ut = mCT.getUtInterface();
// password is not required with Ut interface
- ut.updateCallBarring(getCBTypeFromFacility(facility), action, resp, null);
+ ut.updateCallBarring(getCBTypeFromFacility(facility), action,
+ resp, null, serviceClass);
} catch (ImsException e) {
sendErrorResponse(onComplete, e);
}
@@ -1042,6 +1058,19 @@ public class ImsPhone extends ImsPhoneBase {
break;
case ImsReasonInfo.CODE_FDN_BLOCKED:
error = CommandException.Error.FDN_CHECK_FAILURE;
+ break;
+ case ImsReasonInfo.CODE_UT_SS_MODIFIED_TO_DIAL:
+ error = CommandException.Error.SS_MODIFIED_TO_DIAL;
+ break;
+ case ImsReasonInfo.CODE_UT_SS_MODIFIED_TO_USSD:
+ error = CommandException.Error.SS_MODIFIED_TO_USSD;
+ break;
+ case ImsReasonInfo.CODE_UT_SS_MODIFIED_TO_SS:
+ error = CommandException.Error.SS_MODIFIED_TO_SS;
+ break;
+ case ImsReasonInfo.CODE_UT_SS_MODIFIED_TO_DIAL_VIDEO:
+ error = CommandException.Error.SS_MODIFIED_TO_DIAL_VIDEO;
+ break;
default:
break;
}
@@ -1123,7 +1152,7 @@ public class ImsPhone extends ImsPhoneBase {
* not on the list.
*/
Rlog.d(LOG_TAG, "onMMIDone: mmi=" + mmi);
- if (mPendingMMIs.remove(mmi) || mmi.isUssdRequest()) {
+ if (mPendingMMIs.remove(mmi) || mmi.isUssdRequest() || mmi.isSsInfo()) {
ResultReceiver receiverCallback = mmi.getUssdCallbackReceiver();
if (receiverCallback != null) {
int returnCode = (mmi.getState() == MmiCode.State.COMPLETE) ?
@@ -1209,7 +1238,11 @@ public class ImsPhone extends ImsPhoneBase {
return cfInfo;
}
- private CallForwardInfo[] handleCfQueryResult(ImsCallForwardInfo[] infos) {
+ /**
+ * Used to Convert ImsCallForwardInfo[] to CallForwardInfo[].
+ * Update received call forward status to default IccRecords.
+ */
+ public CallForwardInfo[] handleCfQueryResult(ImsCallForwardInfo[] infos) {
CallForwardInfo[] cfInfos = null;
if (infos != null && infos.length != 0) {
@@ -1597,7 +1630,7 @@ public class ImsPhone extends ImsPhoneBase {
if (imsReasonInfo.mCode == imsReasonInfo.CODE_REGISTRATION_ERROR
&& imsReasonInfo.mExtraMessage != null) {
// Suppress WFC Registration notifications if WFC is not enabled by the user.
- if (ImsManager.isWfcEnabledByUser(mContext)) {
+ if (ImsManager.getInstance(mContext, mPhoneId).isWfcEnabledByUser()) {
processWfcDisconnectForNotification(imsReasonInfo);
}
}
@@ -1683,7 +1716,7 @@ public class ImsPhone extends ImsPhoneBase {
}
// UX requirement is to disable WFC in case of "permanent" registration failures.
- ImsManager.setWfcSetting(mContext, false);
+ ImsManager.getInstance(mContext, mPhoneId).setWfcSetting(false);
// If WfcSettings are active then alert will be shown
// otherwise notification will be added.
@@ -1729,8 +1762,8 @@ public class ImsPhone extends ImsPhoneBase {
if (mCT.getState() == PhoneConstants.State.IDLE) {
if (DBG) Rlog.d(LOG_TAG, "updateRoamingState now: " + newRoaming);
mRoaming = newRoaming;
- ImsManager.setWfcMode(mContext,
- ImsManager.getWfcMode(mContext, newRoaming), newRoaming);
+ ImsManager imsManager = ImsManager.getInstance(mContext, mPhoneId);
+ imsManager.setWfcMode(imsManager.getWfcMode(newRoaming), newRoaming);
} else {
if (DBG) Rlog.d(LOG_TAG, "updateRoamingState postponed: " + newRoaming);
mCT.registerForVoiceCallEnded(this,
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneBase.java b/com/android/internal/telephony/imsphone/ImsPhoneBase.java
index c165b03e..26fe3ae9 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneBase.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneBase.java
@@ -32,6 +32,7 @@ import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import android.util.Pair;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.IccCard;
@@ -104,7 +105,8 @@ abstract class ImsPhoneBase extends Phone {
*
* @param cn The connection.
*/
- protected void startOnHoldTone(Connection cn) {
+ @VisibleForTesting
+ public void startOnHoldTone(Connection cn) {
Pair<Connection, Boolean> result = new Pair<Connection, Boolean>(cn, Boolean.TRUE);
mOnHoldRegistrants.notifyRegistrants(new AsyncResult(null, result, null));
}
@@ -474,12 +476,17 @@ abstract class ImsPhoneBase extends Phone {
}
@Override
- public boolean getDataEnabled() {
+ public boolean isUserDataEnabled() {
return false;
}
@Override
- public void setDataEnabled(boolean enable) {
+ public boolean isDataEnabled() {
+ return false;
+ }
+
+ @Override
+ public void setUserDataEnabled(boolean enable) {
}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneCall.java b/com/android/internal/telephony/imsphone/ImsPhoneCall.java
index 52636f5d..b90f64f0 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneCall.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneCall.java
@@ -237,8 +237,8 @@ public class ImsPhoneCall extends Call {
}
}
- /*package*/ ImsPhoneConnection
- getFirstConnection() {
+ @VisibleForTesting
+ public ImsPhoneConnection getFirstConnection() {
if (mConnections.size() == 0) return null;
return (ImsPhoneConnection) mConnections.get(0);
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java b/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
index 408dca53..05a33cc2 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
@@ -243,6 +243,7 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
private static final int EVENT_GET_IMS_SERVICE = 24;
private static final int EVENT_CHECK_FOR_WIFI_HANDOVER = 25;
private static final int EVENT_ON_FEATURE_CAPABILITY_CHANGED = 26;
+ private static final int EVENT_SUPP_SERVICE_INDICATION = 27;
private static final int TIMEOUT_HANGUP_PENDINGMO = 500;
@@ -293,6 +294,7 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
private int mImsServiceRetryCount;
private ImsManager mImsManager;
+ private ImsUtInterface mUtInterface;
private int mServiceId = -1;
private Call.SrvccState mSrvccState = Call.SrvccState.NONE;
@@ -309,6 +311,7 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
private boolean mAllowEmergencyVideoCalls = false;
private boolean mIgnoreDataEnabledChangedForVideoCalls = false;
private boolean mIsViLteDataMetered = false;
+ private boolean mAlwaysPlayRemoteHoldTone = false;
/**
* Listeners to changes in the phone state. Intended for use by other interested IMS components
@@ -600,7 +603,7 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
/**
* TODO: Remove this code; it is a workaround.
- * When {@code true}, forces {@link ImsManager#updateImsServiceConfig(Context, int, boolean)} to
+ * When {@code true}, forces {@link ImsManager#updateImsServiceConfig(boolean)} to
* be called when an ongoing video call is disconnected. In some cases, where video pause is
* supported by the carrier, when {@link #onDataEnabledChanged(boolean, int)} reports that data
* has been disabled we will pause the video rather than disconnecting the call. When this
@@ -783,9 +786,15 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
mPhone.getExternalCallTracker().getExternalCallStateListener());
}
+ //Set UT interface listener to receive UT indications.
+ mUtInterface = getUtInterface();
+ if (mUtInterface != null) {
+ mUtInterface.registerForSuppServiceIndication(this,
+ EVENT_SUPP_SERVICE_INDICATION, null);
+ }
+
if (mCarrierConfigLoaded) {
- ImsManager.updateImsServiceConfig(mPhone.getContext(),
- mPhone.getPhoneId(), true);
+ mImsManager.updateImsServiceConfig(true);
}
}
@@ -810,6 +819,9 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
mHandoverCall.dispose();
clearDisconnected();
+ if (mUtInterface != null) {
+ mUtInterface.unregisterForSuppServiceIndication(this);
+ }
mPhone.getContext().unregisterReceiver(mReceiver);
mPhone.getDefaultPhone().unregisterForDataEnabledChanged(this);
removeMessages(EVENT_GET_IMS_SERVICE);
@@ -1017,6 +1029,8 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
CarrierConfigManager.KEY_VILTE_DATA_IS_METERED_BOOL);
mSupportPauseVideo = carrierConfig.getBoolean(
CarrierConfigManager.KEY_SUPPORT_PAUSE_IMS_VIDEO_CALLS_BOOL);
+ mAlwaysPlayRemoteHoldTone = carrierConfig.getBoolean(
+ CarrierConfigManager.KEY_ALWAYS_PLAY_REMOTE_HOLD_TONE_BOOL);
String[] mappings = carrierConfig
.getStringArray(CarrierConfigManager.KEY_IMS_REASONINFO_MAPPING_STRING_ARRAY);
@@ -1934,6 +1948,9 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
}
}
+ case ImsReasonInfo.CODE_CALL_BARRED:
+ return DisconnectCause.CALL_BARRED;
+
case ImsReasonInfo.CODE_FDN_BLOCKED:
return DisconnectCause.FDN_BLOCKED;
@@ -1967,6 +1984,30 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
case ImsReasonInfo.CODE_EMERGENCY_PERM_FAILURE:
return DisconnectCause.EMERGENCY_PERM_FAILURE;
+ case ImsReasonInfo.CODE_DIAL_MODIFIED_TO_USSD:
+ return DisconnectCause.DIAL_MODIFIED_TO_USSD;
+
+ case ImsReasonInfo.CODE_DIAL_MODIFIED_TO_SS:
+ return DisconnectCause.DIAL_MODIFIED_TO_SS;
+
+ case ImsReasonInfo.CODE_DIAL_MODIFIED_TO_DIAL:
+ return DisconnectCause.DIAL_MODIFIED_TO_DIAL;
+
+ case ImsReasonInfo.CODE_DIAL_MODIFIED_TO_DIAL_VIDEO:
+ return DisconnectCause.DIAL_MODIFIED_TO_DIAL_VIDEO;
+
+ case ImsReasonInfo.CODE_DIAL_VIDEO_MODIFIED_TO_DIAL:
+ return DisconnectCause.DIAL_VIDEO_MODIFIED_TO_DIAL;
+
+ case ImsReasonInfo.CODE_DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO:
+ return DisconnectCause.DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO;
+
+ case ImsReasonInfo.CODE_DIAL_VIDEO_MODIFIED_TO_SS:
+ return DisconnectCause.DIAL_VIDEO_MODIFIED_TO_SS;
+
+ case ImsReasonInfo.CODE_DIAL_VIDEO_MODIFIED_TO_USSD:
+ return DisconnectCause.DIAL_VIDEO_MODIFIED_TO_USSD;
+
default:
}
@@ -2231,7 +2272,9 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
if (mShouldUpdateImsConfigOnDisconnect) {
// Ensure we update the IMS config when the call is disconnected; we delayed this
// because a video call was paused.
- ImsManager.updateImsServiceConfig(mPhone.getContext(), mPhone.getPhoneId(), true);
+ if (mImsManager != null) {
+ mImsManager.updateImsServiceConfig(true);
+ }
mShouldUpdateImsConfigOnDisconnect = false;
}
}
@@ -2406,38 +2449,7 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
@Override
public void onCallHoldReceived(ImsCall imsCall) {
- if (DBG) log("onCallHoldReceived");
-
- ImsPhoneConnection conn = findConnection(imsCall);
- if (conn != null) {
- if (!mOnHoldToneStarted && ImsPhoneCall.isLocalTone(imsCall) &&
- conn.getState() == ImsPhoneCall.State.ACTIVE) {
- mPhone.startOnHoldTone(conn);
- mOnHoldToneStarted = true;
- mOnHoldToneId = System.identityHashCode(conn);
- }
- conn.onConnectionEvent(android.telecom.Connection.EVENT_CALL_REMOTELY_HELD, null);
-
- boolean useVideoPauseWorkaround = mPhone.getContext().getResources().getBoolean(
- com.android.internal.R.bool.config_useVideoPauseWorkaround);
- if (useVideoPauseWorkaround && mSupportPauseVideo &&
- VideoProfile.isVideo(conn.getVideoState())) {
- // If we are using the video pause workaround, the vendor IMS code has issues
- // with video pause signalling. In this case, when a call is remotely
- // held, the modem does not reliably change the video state of the call to be
- // paused.
- // As a workaround, we will turn on that bit now.
- conn.changeToPausedState();
- }
- }
-
- SuppServiceNotification supp = new SuppServiceNotification();
- // Type of notification: 0 = MO; 1 = MT
- // Refer SuppServiceNotification class documentation.
- supp.notificationType = 1;
- supp.code = SuppServiceNotification.MT_CODE_CALL_ON_HOLD;
- mPhone.notifySuppSvcNotification(supp);
- mMetrics.writeOnImsCallHoldReceived(mPhone.getPhoneId(), imsCall.getCallSession());
+ ImsPhoneCallTracker.this.onCallHoldReceived(imsCall);
}
@Override
@@ -2567,9 +2579,17 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
&& targetAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
if (isHandoverFromWifi && imsCall.isVideoCall()) {
if (mNotifyHandoverVideoFromWifiToLTE && mIsDataEnabled) {
- log("onCallHandover :: notifying of WIFI to LTE handover.");
- conn.onConnectionEvent(
- TelephonyManager.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE, null);
+ if (conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
+ log("onCallHandover :: notifying of WIFI to LTE handover.");
+ conn.onConnectionEvent(
+ TelephonyManager.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE, null);
+ } else {
+ // Call has already had a disconnect request issued by the user or is
+ // in the process of disconnecting; do not inform the UI of this as it
+ // is not relevant.
+ log("onCallHandover :: skip notify of WIFI to LTE handover for "
+ + "disconnected call.");
+ }
}
if (!mIsDataEnabled && mIsViLteDataMetered) {
@@ -2978,6 +2998,17 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
}
break;
}
+ case EVENT_SUPP_SERVICE_INDICATION: {
+ ar = (AsyncResult) msg.obj;
+ ImsPhoneMmiCode mmiCode = new ImsPhoneMmiCode(mPhone);
+ try {
+ mmiCode.setIsSsInfo(true);
+ mmiCode.processImsSsData(ar);
+ } catch (ImsException e) {
+ Rlog.e(LOG_TAG, "Exception in parsing SS Data: " + e);
+ }
+ break;
+ }
}
}
@@ -3330,8 +3361,8 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
boolean isActiveCallVideo = activeCall.isVideoCall() ||
(mTreatDowngradedVideoCallsAsVideoCalls && activeCall.wasVideoCall());
boolean isActiveCallOnWifi = activeCall.isWifiCall();
- boolean isVoWifiEnabled = mImsManager.isWfcEnabledByPlatform(mPhone.getContext()) &&
- mImsManager.isWfcEnabledByUser(mPhone.getContext());
+ boolean isVoWifiEnabled = mImsManager.isWfcEnabledByPlatform()
+ && mImsManager.isWfcEnabledByUser();
boolean isIncomingCallAudio = !incomingCall.isVideoCall();
log("shouldDisconnectActiveCallOnAnswer : isActiveCallVideo=" + isActiveCallVideo +
" isActiveCallOnWifi=" + isActiveCallOnWifi + " isIncomingCallAudio=" +
@@ -3411,7 +3442,6 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
log("onDataEnabledChanged: enabled=" + enabled + ", reason=" + reason);
- ImsManager.getInstance(mPhone.getContext(), mPhone.getPhoneId()).setDataEnabled(enabled);
mIsDataEnabled = enabled;
if (!mIsViLteDataMetered) {
@@ -3448,7 +3478,9 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
&& reason != DataEnabledSettings.REASON_REGISTERED) {
// This will call into updateVideoCallFeatureValue and eventually all clients will be
// asynchronously notified that the availability of VT over LTE has changed.
- ImsManager.updateImsServiceConfig(mPhone.getContext(), mPhone.getPhoneId(), true);
+ if (mImsManager != null) {
+ mImsManager.updateImsServiceConfig(true);
+ }
}
}
@@ -3646,4 +3678,46 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
mPhone.getPhoneId(), mImsFeatureEnabled);
}
}
+
+ @VisibleForTesting
+ public void onCallHoldReceived(ImsCall imsCall) {
+ if (DBG) log("onCallHoldReceived");
+
+ ImsPhoneConnection conn = findConnection(imsCall);
+ if (conn != null) {
+ if (!mOnHoldToneStarted && (ImsPhoneCall.isLocalTone(imsCall)
+ || mAlwaysPlayRemoteHoldTone) &&
+ conn.getState() == ImsPhoneCall.State.ACTIVE) {
+ mPhone.startOnHoldTone(conn);
+ mOnHoldToneStarted = true;
+ mOnHoldToneId = System.identityHashCode(conn);
+ }
+ conn.onConnectionEvent(android.telecom.Connection.EVENT_CALL_REMOTELY_HELD, null);
+
+ boolean useVideoPauseWorkaround = mPhone.getContext().getResources().getBoolean(
+ com.android.internal.R.bool.config_useVideoPauseWorkaround);
+ if (useVideoPauseWorkaround && mSupportPauseVideo &&
+ VideoProfile.isVideo(conn.getVideoState())) {
+ // If we are using the video pause workaround, the vendor IMS code has issues
+ // with video pause signalling. In this case, when a call is remotely
+ // held, the modem does not reliably change the video state of the call to be
+ // paused.
+ // As a workaround, we will turn on that bit now.
+ conn.changeToPausedState();
+ }
+ }
+
+ SuppServiceNotification supp = new SuppServiceNotification();
+ // Type of notification: 0 = MO; 1 = MT
+ // Refer SuppServiceNotification class documentation.
+ supp.notificationType = 1;
+ supp.code = SuppServiceNotification.MT_CODE_CALL_ON_HOLD;
+ mPhone.notifySuppSvcNotification(supp);
+ mMetrics.writeOnImsCallHoldReceived(mPhone.getPhoneId(), imsCall.getCallSession());
+ }
+
+ @VisibleForTesting
+ public void setAlwaysPlayRemoteHoldTone(boolean shouldPlayRemoteHoldTone) {
+ mAlwaysPlayRemoteHoldTone = shouldPlayRemoteHoldTone;
+ }
}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java b/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java
index c2711e8d..2683a802 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java
@@ -50,6 +50,14 @@ class ImsPhoneCommandInterface extends BaseCommands implements CommandsInterface
}
@Override
+ public void getIccSlotsStatus(Message result) {
+ }
+
+ @Override
+ public void setLogicalToPhysicalSlotMapping(int[] physicalSlots, Message result) {
+ }
+
+ @Override
public void supplyIccPin(String pin, Message result) {
}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneConnection.java b/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
index 9800a444..3c3c1922 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
@@ -781,8 +781,8 @@ public class ImsPhoneConnection extends Connection implements
callProfile.getCallExtraInt(ImsCallProfile.EXTRA_CNAP));
if (Phone.DEBUG_PHONE) {
Rlog.d(LOG_TAG, "updateAddressDisplay: callId = " + getTelecomCallId()
- + " address = " + Rlog.pii(LOG_TAG, address) + " name = " + name
- + " nump = " + nump + " namep = " + namep);
+ + " address = " + Rlog.pii(LOG_TAG, address) + " name = "
+ + Rlog.pii(LOG_TAG, name) + " nump = " + nump + " namep = " + namep);
}
if (!mIsMergeInProcess) {
// Only process changes to the name and address when a merge is not in process.
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java b/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
index 9c99055c..45b12ea2 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
@@ -41,6 +41,7 @@ import android.text.TextUtils;
import com.android.ims.ImsException;
import com.android.ims.ImsReasonInfo;
+import com.android.ims.ImsSsData;
import com.android.ims.ImsSsInfo;
import com.android.ims.ImsUtInterface;
import com.android.internal.telephony.CallForwardInfo;
@@ -49,6 +50,7 @@ import com.android.internal.telephony.CommandException;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.MmiCode;
import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.RILConstants;
import com.android.internal.telephony.uicc.IccRecords;
import java.util.regex.Matcher;
@@ -185,6 +187,7 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
private boolean mIsCallFwdReg;
private State mState = State.PENDING;
private CharSequence mMessage;
+ private boolean mIsSsInfo = false;
//resgister/erasure of ICB (Specific DN)
static final String IcbDnMmi = "Specific Incoming Call Barring";
//ICB (Anonymous)
@@ -496,7 +499,7 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
//***** Constructor
- ImsPhoneMmiCode(ImsPhone phone) {
+ public ImsPhoneMmiCode(ImsPhone phone) {
// The telephony unit-test cases may create ImsPhoneMmiCode's
// in secondary threads
super(phone.getHandler().getLooper());
@@ -736,7 +739,9 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
try {
int serviceClass = siToServiceClass(mSib);
if (serviceClass != SERVICE_CLASS_NONE
- && serviceClass != SERVICE_CLASS_VOICE) {
+ && serviceClass != SERVICE_CLASS_VOICE
+ && serviceClass != (SERVICE_CLASS_PACKET
+ + SERVICE_CLASS_DATA_SYNC)) {
return false;
}
return true;
@@ -845,13 +850,14 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
String password = mSia;
String facility = scToBarringFacility(mSc);
+ int serviceClass = siToServiceClass(mSib);
if (isInterrogate()) {
mPhone.getCallBarring(facility,
- obtainMessage(EVENT_SUPP_SVC_QUERY_COMPLETE, this));
+ obtainMessage(EVENT_SUPP_SVC_QUERY_COMPLETE, this), serviceClass);
} else if (isActivate() || isDeactivate()) {
mPhone.setCallBarring(facility, isActivate(), password,
- obtainMessage(EVENT_SET_COMPLETE, this));
+ obtainMessage(EVENT_SET_COMPLETE, this), serviceClass);
} else {
throw new RuntimeException ("Invalid or Unsupported MMI Code");
}
@@ -1173,15 +1179,42 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
}
private CharSequence getErrorMessage(AsyncResult ar) {
- if (ar.exception instanceof CommandException) {
- CommandException.Error err = ((CommandException) (ar.exception)).getCommandError();
- if (err == CommandException.Error.FDN_CHECK_FAILURE) {
- Rlog.i(LOG_TAG, "FDN_CHECK_FAILURE");
+ CharSequence errorMessage;
+ return ((errorMessage = getMmiErrorMessage(ar)) != null) ? errorMessage :
+ mContext.getText(com.android.internal.R.string.mmiError);
+ }
+
+ private CharSequence getMmiErrorMessage(AsyncResult ar) {
+ if (ar.exception instanceof ImsException) {
+ switch (((ImsException) ar.exception).getCode()) {
+ case ImsReasonInfo.CODE_FDN_BLOCKED:
+ return mContext.getText(com.android.internal.R.string.mmiFdnError);
+ case ImsReasonInfo.CODE_UT_SS_MODIFIED_TO_DIAL:
+ return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_dial);
+ case ImsReasonInfo.CODE_UT_SS_MODIFIED_TO_USSD:
+ return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_ussd);
+ case ImsReasonInfo.CODE_UT_SS_MODIFIED_TO_SS:
+ return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_ss);
+ case ImsReasonInfo.CODE_UT_SS_MODIFIED_TO_DIAL_VIDEO:
+ return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_dial_video);
+ default:
+ return null;
+ }
+ } else if (ar.exception instanceof CommandException) {
+ CommandException err = (CommandException) ar.exception;
+ if (err.getCommandError() == CommandException.Error.FDN_CHECK_FAILURE) {
return mContext.getText(com.android.internal.R.string.mmiFdnError);
+ } else if (err.getCommandError() == CommandException.Error.SS_MODIFIED_TO_DIAL) {
+ return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_dial);
+ } else if (err.getCommandError() == CommandException.Error.SS_MODIFIED_TO_USSD) {
+ return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_ussd);
+ } else if (err.getCommandError() == CommandException.Error.SS_MODIFIED_TO_SS) {
+ return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_ss);
+ } else if (err.getCommandError() == CommandException.Error.SS_MODIFIED_TO_DIAL_VIDEO) {
+ return mContext.getText(com.android.internal.R.string.stk_cc_ss_to_dial_video);
}
}
-
- return mContext.getText(com.android.internal.R.string.mmiError);
+ return null;
}
private CharSequence getScString() {
@@ -1222,11 +1255,12 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
if (ar.exception instanceof CommandException) {
CommandException err = (CommandException) ar.exception;
+ CharSequence errorMessage;
if (err.getCommandError() == CommandException.Error.PASSWORD_INCORRECT) {
sb.append(mContext.getText(
com.android.internal.R.string.passwordIncorrect));
- } else if (err.getCommandError() == CommandException.Error.FDN_CHECK_FAILURE) {
- sb.append(mContext.getText(com.android.internal.R.string.mmiFdnError));
+ } else if ((errorMessage = getMmiErrorMessage(ar)) != null) {
+ sb.append(errorMessage);
} else if (err.getMessage() != null) {
sb.append(err.getMessage());
} else {
@@ -1376,7 +1410,7 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
infos = (CallForwardInfo[]) ar.result;
- if (infos.length == 0) {
+ if (infos == null || infos.length == 0) {
// Assume the default is not active
sb.append(mContext.getText(com.android.internal.R.string.serviceDisabled));
@@ -1656,8 +1690,9 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
private CharSequence getImsErrorMessage(AsyncResult ar) {
ImsException error = (ImsException) ar.exception;
- if (error.getCode() == ImsReasonInfo.CODE_FDN_BLOCKED) {
- return mContext.getText(com.android.internal.R.string.mmiFdnError);
+ CharSequence errorMessage;
+ if ((errorMessage = getMmiErrorMessage(ar)) != null) {
+ return errorMessage;
} else if (error.getMessage() != null) {
return error.getMessage();
} else {
@@ -1670,6 +1705,164 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
return this.mCallbackReceiver;
}
+ /**
+ * Process IMS SS Data received.
+ */
+ public void processImsSsData(AsyncResult data) throws ImsException {
+ try {
+ ImsSsData ssData = (ImsSsData) data.result;
+ parseSsData(ssData);
+ } catch (ClassCastException | NullPointerException ex) {
+ throw new ImsException("Exception in parsing SS Data", 0);
+ }
+ }
+
+ void parseSsData(ImsSsData ssData) {
+ ImsException ex = (ssData.result != RILConstants.SUCCESS)
+ ? new ImsException(null, ssData.result) : null;
+ mSc = getScStringFromScType(ssData.serviceType);
+ mAction = getActionStringFromReqType(ssData.requestType);
+ Rlog.d(LOG_TAG, "parseSsData msc = " + mSc + ", action = " + mAction + ", ex = " + ex);
+
+ switch (ssData.requestType) {
+ case ImsSsData.SS_ACTIVATION:
+ case ImsSsData.SS_DEACTIVATION:
+ case ImsSsData.SS_REGISTRATION:
+ case ImsSsData.SS_ERASURE:
+ if ((ssData.result == RILConstants.SUCCESS)
+ && ssData.isTypeUnConditional()) {
+ /*
+ * When ssData.serviceType is unconditional (SS_CFU or SS_CF_ALL) and
+ * ssData.requestType is activate/register and
+ * ServiceClass is Voice/Video/None, turn on voice call forwarding.
+ */
+ boolean cffEnabled = ((ssData.requestType == ImsSsData.SS_ACTIVATION
+ || ssData.requestType == ImsSsData.SS_REGISTRATION)
+ && isServiceClassVoiceVideoOrNone(ssData.serviceClass));
+
+ Rlog.d(LOG_TAG, "setCallForwardingFlag cffEnabled: " + cffEnabled);
+ if (mIccRecords != null) {
+ Rlog.d(LOG_TAG, "setVoiceCallForwardingFlag done from SS Info.");
+ //Only CF status is set here as part of activation/registration,
+ //number is not available until interrogation.
+ mPhone.setVoiceCallForwardingFlag(1, cffEnabled, null);
+ } else {
+ Rlog.e(LOG_TAG, "setCallForwardingFlag aborted. sim records is null.");
+ }
+ }
+ onSetComplete(null, new AsyncResult(null, ssData.cfInfo, ex));
+ break;
+ case ImsSsData.SS_INTERROGATION:
+ if (ssData.isTypeClir()) {
+ Rlog.d(LOG_TAG, "CLIR INTERROGATION");
+ Bundle clirInfo = new Bundle();
+ clirInfo.putIntArray(UT_BUNDLE_KEY_CLIR, ssData.ssInfo);
+ onQueryClirComplete(new AsyncResult(null, clirInfo, ex));
+ } else if (ssData.isTypeCF()) {
+ Rlog.d(LOG_TAG, "CALL FORWARD INTERROGATION");
+ onQueryCfComplete(new AsyncResult(null, mPhone
+ .handleCfQueryResult(ssData.cfInfo), ex));
+ } else if (ssData.isTypeBarring()) {
+ onSuppSvcQueryComplete(new AsyncResult(null, ssData.ssInfo, ex));
+ } else if (ssData.isTypeColr() || ssData.isTypeClip() || ssData.isTypeColp()) {
+ ImsSsInfo ssInfo = new ImsSsInfo();
+ ssInfo.mStatus = ssData.ssInfo[0];
+ Bundle clInfo = new Bundle();
+ clInfo.putParcelable(UT_BUNDLE_KEY_SSINFO, ssInfo);
+ onSuppSvcQueryComplete(new AsyncResult(null, clInfo, ex));
+ } else if (ssData.isTypeIcb()) {
+ onIcbQueryComplete(new AsyncResult(null, ssData.imsSsInfo, ex));
+ } else {
+ onQueryComplete(new AsyncResult(null, ssData.ssInfo, ex));
+ }
+ break;
+ default:
+ Rlog.e(LOG_TAG, "Invaid requestType in SSData : " + ssData.requestType);
+ break;
+ }
+ }
+
+ private String getScStringFromScType(int serviceType) {
+ switch (serviceType) {
+ case ImsSsData.SS_CFU:
+ return SC_CFU;
+ case ImsSsData.SS_CF_BUSY:
+ return SC_CFB;
+ case ImsSsData.SS_CF_NO_REPLY:
+ return SC_CFNRy;
+ case ImsSsData.SS_CF_NOT_REACHABLE:
+ return SC_CFNR;
+ case ImsSsData.SS_CF_ALL:
+ return SC_CF_All;
+ case ImsSsData.SS_CF_ALL_CONDITIONAL:
+ return SC_CF_All_Conditional;
+ case ImsSsData.SS_CLIP:
+ return SC_CLIP;
+ case ImsSsData.SS_CLIR:
+ return SC_CLIR;
+ case ImsSsData.SS_COLP:
+ return SC_COLP;
+ case ImsSsData.SS_COLR:
+ return SC_COLR;
+ case ImsSsData.SS_CNAP:
+ return SC_CNAP;
+ case ImsSsData.SS_WAIT:
+ return SC_WAIT;
+ case ImsSsData.SS_BAOC:
+ return SC_BAOC;
+ case ImsSsData.SS_BAOIC:
+ return SC_BAOIC;
+ case ImsSsData.SS_BAOIC_EXC_HOME:
+ return SC_BAOICxH;
+ case ImsSsData.SS_BAIC:
+ return SC_BAIC;
+ case ImsSsData.SS_BAIC_ROAMING:
+ return SC_BAICr;
+ case ImsSsData.SS_ALL_BARRING:
+ return SC_BA_ALL;
+ case ImsSsData.SS_OUTGOING_BARRING:
+ return SC_BA_MO;
+ case ImsSsData.SS_INCOMING_BARRING:
+ return SC_BA_MT;
+ case ImsSsData.SS_INCOMING_BARRING_DN:
+ return SC_BS_MT;
+ case ImsSsData.SS_INCOMING_BARRING_ANONYMOUS:
+ return SC_BAICa;
+ default:
+ return null;
+ }
+ }
+
+ private String getActionStringFromReqType(int requestType) {
+ switch (requestType) {
+ case ImsSsData.SS_ACTIVATION:
+ return ACTION_ACTIVATE;
+ case ImsSsData.SS_DEACTIVATION:
+ return ACTION_DEACTIVATE;
+ case ImsSsData.SS_INTERROGATION:
+ return ACTION_INTERROGATE;
+ case ImsSsData.SS_REGISTRATION:
+ return ACTION_REGISTER;
+ case ImsSsData.SS_ERASURE:
+ return ACTION_ERASURE;
+ default:
+ return null;
+ }
+ }
+
+ private boolean isServiceClassVoiceVideoOrNone(int serviceClass) {
+ return ((serviceClass == SERVICE_CLASS_NONE) || (serviceClass == SERVICE_CLASS_VOICE)
+ || (serviceClass == (SERVICE_CLASS_PACKET + SERVICE_CLASS_DATA_SYNC)));
+ }
+
+ public boolean isSsInfo() {
+ return mIsSsInfo;
+ }
+
+ public void setIsSsInfo(boolean isSsInfo) {
+ mIsSsInfo = isSsInfo;
+ }
+
/***
* TODO: It would be nice to have a method here that can take in a dialstring and
* figure out if there is an MMI code embedded within it. This code would replace
diff --git a/com/android/internal/telephony/metrics/TelephonyMetrics.java b/com/android/internal/telephony/metrics/TelephonyMetrics.java
index bbb595cb..ba41bea5 100644
--- a/com/android/internal/telephony/metrics/TelephonyMetrics.java
+++ b/com/android/internal/telephony/metrics/TelephonyMetrics.java
@@ -23,8 +23,7 @@ import static com.android.internal.telephony.RILConstants.RIL_REQUEST_CDMA_SEND_
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_DEACTIVATE_DATA_CALL;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_DIAL;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_HANGUP;
-import static com.android.internal.telephony.RILConstants
- .RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND;
+import static com.android.internal.telephony.RILConstants.RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_IMS_SEND_SMS;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SEND_SMS;
@@ -41,6 +40,7 @@ import android.os.SystemClock;
import android.telephony.Rlog;
import android.telephony.ServiceState;
import android.telephony.TelephonyHistogram;
+import android.telephony.data.DataCallResponse;
import android.text.TextUtils;
import android.util.Base64;
import android.util.SparseArray;
@@ -54,7 +54,6 @@ import com.android.internal.telephony.RIL;
import com.android.internal.telephony.RILConstants;
import com.android.internal.telephony.SmsResponse;
import com.android.internal.telephony.UUSInfo;
-import com.android.internal.telephony.dataconnection.DataCallResponse;
import com.android.internal.telephony.imsphone.ImsPhoneCall;
import com.android.internal.telephony.nano.TelephonyProto;
import com.android.internal.telephony.nano.TelephonyProto.ImsCapabilities;
@@ -1059,12 +1058,12 @@ public class TelephonyMetrics {
for (int i = 0; i < dcsList.size(); i++) {
dataCalls[i] = new RilDataCall();
- dataCalls[i].cid = dcsList.get(i).cid;
- if (!TextUtils.isEmpty(dcsList.get(i).ifname)) {
- dataCalls[i].iframe = dcsList.get(i).ifname;
+ dataCalls[i].cid = dcsList.get(i).getCallId();
+ if (!TextUtils.isEmpty(dcsList.get(i).getIfname())) {
+ dataCalls[i].iframe = dcsList.get(i).getIfname();
}
- if (!TextUtils.isEmpty(dcsList.get(i).type)) {
- dataCalls[i].type = toPdpType(dcsList.get(i).type);
+ if (!TextUtils.isEmpty(dcsList.get(i).getType())) {
+ dataCalls[i].type = toPdpType(dcsList.get(i).getType());
}
}
@@ -1307,17 +1306,17 @@ public class TelephonyMetrics {
RilDataCall dataCall = new RilDataCall();
if (response != null) {
- setupDataCallResponse.status =
- (response.status == 0 ? RilDataCallFailCause.PDP_FAIL_NONE : response.status);
- setupDataCallResponse.suggestedRetryTimeMillis = response.suggestedRetryTime;
+ setupDataCallResponse.status = (response.getStatus() == 0
+ ? RilDataCallFailCause.PDP_FAIL_NONE : response.getStatus());
+ setupDataCallResponse.suggestedRetryTimeMillis = response.getSuggestedRetryTime();
- dataCall.cid = response.cid;
- if (!TextUtils.isEmpty(response.type)) {
- dataCall.type = toPdpType(response.type);
+ dataCall.cid = response.getCallId();
+ if (!TextUtils.isEmpty(response.getType())) {
+ dataCall.type = toPdpType(response.getType());
}
- if (!TextUtils.isEmpty(response.ifname)) {
- dataCall.iframe = response.ifname;
+ if (!TextUtils.isEmpty(response.getIfname())) {
+ dataCall.iframe = response.getIfname();
}
}
setupDataCallResponse.call = dataCall;
diff --git a/com/android/internal/telephony/sip/SipCommandInterface.java b/com/android/internal/telephony/sip/SipCommandInterface.java
index 59af1bbd..69832e88 100644
--- a/com/android/internal/telephony/sip/SipCommandInterface.java
+++ b/com/android/internal/telephony/sip/SipCommandInterface.java
@@ -49,6 +49,14 @@ class SipCommandInterface extends BaseCommands implements CommandsInterface {
}
@Override
+ public void getIccSlotsStatus(Message result) {
+ }
+
+ @Override
+ public void setLogicalToPhysicalSlotMapping(int[] physicalSlots, Message result) {
+ }
+
+ @Override
public void supplyIccPin(String pin, Message result) {
}
diff --git a/com/android/internal/telephony/sip/SipPhoneBase.java b/com/android/internal/telephony/sip/SipPhoneBase.java
index 7bdfad8b..c2160dd0 100644
--- a/com/android/internal/telephony/sip/SipPhoneBase.java
+++ b/com/android/internal/telephony/sip/SipPhoneBase.java
@@ -445,12 +445,17 @@ abstract class SipPhoneBase extends Phone {
}
@Override
- public boolean getDataEnabled() {
+ public boolean isUserDataEnabled() {
return false;
}
@Override
- public void setDataEnabled(boolean enable) {
+ public boolean isDataEnabled() {
+ return false;
+ }
+
+ @Override
+ public void setUserDataEnabled(boolean enable) {
}
public boolean enableDataConnectivity() {
diff --git a/com/android/internal/telephony/test/SimulatedCommands.java b/com/android/internal/telephony/test/SimulatedCommands.java
index b90a2924..62938220 100644
--- a/com/android/internal/telephony/test/SimulatedCommands.java
+++ b/com/android/internal/telephony/test/SimulatedCommands.java
@@ -18,6 +18,7 @@ package com.android.internal.telephony.test;
import android.hardware.radio.V1_0.DataRegStateResult;
import android.hardware.radio.V1_0.VoiceRegStateResult;
+import android.net.NetworkUtils;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.HandlerThread;
@@ -35,7 +36,9 @@ import android.telephony.NetworkScanRequest;
import android.telephony.Rlog;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
+import android.telephony.data.DataCallResponse;
import android.telephony.data.DataProfile;
+import android.telephony.data.InterfaceAddress;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.BaseCommands;
@@ -49,13 +52,14 @@ import com.android.internal.telephony.RadioCapability;
import com.android.internal.telephony.SmsResponse;
import com.android.internal.telephony.UUSInfo;
import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
-import com.android.internal.telephony.dataconnection.DataCallResponse;
import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
import com.android.internal.telephony.gsm.SuppServiceNotification;
import com.android.internal.telephony.uicc.IccCardStatus;
import com.android.internal.telephony.uicc.IccIoResult;
+import com.android.internal.telephony.uicc.IccSlotStatus;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -122,6 +126,7 @@ public class SimulatedCommands extends BaseCommands
private List<CellInfo> mCellInfoList;
private int[] mImsRegState;
private IccCardStatus mIccCardStatus;
+ private IccSlotStatus mIccSlotStatus;
private IccIoResult mIccIoResultForApduLogicalChannel;
private int mChannelId = IccOpenLogicalChannelResponse.INVALID_CHANNEL;
@@ -167,13 +172,31 @@ public class SimulatedCommands extends BaseCommands
@Override
public void getIccCardStatus(Message result) {
- if(mIccCardStatus!=null) {
+ if (mIccCardStatus != null) {
resultSuccess(result, mIccCardStatus);
} else {
resultFail(result, null, new RuntimeException("IccCardStatus not set"));
}
}
+ public void setIccSlotStatus(IccSlotStatus iccSlotStatus) {
+ mIccSlotStatus = iccSlotStatus;
+ }
+
+ @Override
+ public void getIccSlotsStatus(Message result) {
+ if (mIccSlotStatus != null) {
+ resultSuccess(result, mIccSlotStatus);
+ } else {
+ resultFail(result, null, new RuntimeException("IccSlotStatus not set"));
+ }
+ }
+
+ @Override
+ public void setLogicalToPhysicalSlotMapping(int[] physicalSlots, Message result) {
+ unimplemented(result);
+ }
+
@Override
public void supplyIccPin(String pin, Message result) {
if (mSimLockedState != SimLockState.REQUIRE_PIN) {
@@ -1104,8 +1127,15 @@ public class SimulatedCommands extends BaseCommands
isRoaming, allowRoaming, result);
if (mDcResponse == null) {
- mDcResponse = new DataCallResponse(0, -1, 1, 2, "IP", "rmnet_data7",
- "12.34.56.78", "98.76.54.32", "11.22.33.44", "", 1440);
+ try {
+ mDcResponse = new DataCallResponse(0, -1, 1, 2, "IP", "rmnet_data7",
+ Arrays.asList(new InterfaceAddress("12.34.56.78", 0)),
+ Arrays.asList(NetworkUtils.numericToInetAddress("98.76.54.32")),
+ Arrays.asList(NetworkUtils.numericToInetAddress("11.22.33.44")),
+ null, 1440);
+ } catch (Exception e) {
+
+ }
}
if (mDcSuccess) {
diff --git a/com/android/internal/telephony/test/SimulatedCommandsVerifier.java b/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
index c51b6adb..40b15a97 100644
--- a/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
+++ b/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
@@ -136,6 +136,16 @@ public class SimulatedCommandsVerifier implements CommandsInterface {
}
@Override
+ public void registerForIccSlotStatusChanged(Handler h, int what, Object obj) {
+
+ }
+
+ @Override
+ public void unregisterForIccSlotStatusChanged(Handler h) {
+
+ }
+
+ @Override
public void registerForCallStateChanged(Handler h, int what, Object obj) {
}
@@ -1166,6 +1176,14 @@ public class SimulatedCommandsVerifier implements CommandsInterface {
}
@Override
+ public void getIccSlotsStatus(Message result) {
+ }
+
+ @Override
+ public void setLogicalToPhysicalSlotMapping(int[] physicalSlots, Message result) {
+ }
+
+ @Override
public int getLteOnCdmaMode() {
return 0;
}
diff --git a/com/android/internal/telephony/uicc/CarrierTestOverride.java b/com/android/internal/telephony/uicc/CarrierTestOverride.java
index 755b79f0..2e725005 100644
--- a/com/android/internal/telephony/uicc/CarrierTestOverride.java
+++ b/com/android/internal/telephony/uicc/CarrierTestOverride.java
@@ -48,6 +48,7 @@ public class CarrierTestOverride {
<carrierTestOverride key="imsi" value="310010123456789"/>
<carrierTestOverride key="spn" value="Verizon"/>
<carrierTestOverride key="pnn" value="Verizon network"/>
+ <carrierTestOverride key="iccid" value="123456789012345678901"/>
</carrierTestOverrides>
*/
static final String DATA_CARRIER_TEST_OVERRIDE_PATH =
@@ -62,6 +63,7 @@ public class CarrierTestOverride {
static final String CARRIER_TEST_XML_ITEM_KEY_STRING_IMSI = "imsi";
static final String CARRIER_TEST_XML_ITEM_KEY_STRING_SPN = "spn";
static final String CARRIER_TEST_XML_ITEM_KEY_STRING_PNN = "pnn";
+ static final String CARRIER_TEST_XML_ITEM_KEY_STRING_ICCID = "iccid";
private HashMap<String, String> mCarrierTestParamMap;
@@ -131,6 +133,17 @@ public class CarrierTestOverride {
}
}
+ String getFakeIccid() {
+ try {
+ String iccid = mCarrierTestParamMap.get(CARRIER_TEST_XML_ITEM_KEY_STRING_ICCID);
+ Rlog.d(LOG_TAG, "reading iccid from CarrierTestConfig file: " + iccid);
+ return iccid;
+ } catch (NullPointerException e) {
+ Rlog.w(LOG_TAG, "No iccid in CarrierTestConfig file ");
+ return null;
+ }
+ }
+
private void loadCarrierTestOverrides() {
FileReader carrierTestConfigReader;
diff --git a/com/android/internal/telephony/uicc/IccCardProxy.java b/com/android/internal/telephony/uicc/IccCardProxy.java
index 241f2112..f3c5ca5f 100644
--- a/com/android/internal/telephony/uicc/IccCardProxy.java
+++ b/com/android/internal/telephony/uicc/IccCardProxy.java
@@ -38,11 +38,9 @@ import com.android.internal.telephony.IccCardConstants;
import com.android.internal.telephony.IccCardConstants.State;
import com.android.internal.telephony.IntentBroadcaster;
import com.android.internal.telephony.MccTable;
-import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.RILConstants;
import com.android.internal.telephony.TelephonyIntents;
-import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager;
import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
import com.android.internal.telephony.uicc.IccCardApplicationStatus.PersoSubState;
import com.android.internal.telephony.uicc.IccCardStatus.CardState;
@@ -82,7 +80,6 @@ public class IccCardProxy extends Handler implements IccCard {
private static final int EVENT_RECORDS_LOADED = 7;
private static final int EVENT_IMSI_READY = 8;
private static final int EVENT_NETWORK_LOCKED = 9;
- private static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 11;
private static final int EVENT_ICC_RECORD_EVENTS = 500;
private static final int EVENT_SUBSCRIPTION_ACTIVATED = 501;
@@ -96,19 +93,15 @@ public class IccCardProxy extends Handler implements IccCard {
private CommandsInterface mCi;
private TelephonyManager mTelephonyManager;
- private RegistrantList mAbsentRegistrants = new RegistrantList();
- private RegistrantList mPinLockedRegistrants = new RegistrantList();
private RegistrantList mNetworkLockedRegistrants = new RegistrantList();
private int mCurrentAppType = UiccController.APP_FAM_3GPP; //default to 3gpp?
private UiccController mUiccController = null;
+ private UiccSlot mUiccSlot = null;
private UiccCard mUiccCard = null;
private UiccCardApplication mUiccApplication = null;
private IccRecords mIccRecords = null;
- private CdmaSubscriptionSourceManager mCdmaSSM = null;
private RadioState mRadioState = RadioState.RADIO_UNAVAILABLE;
- private boolean mQuietMode = false; // when set to true IccCardProxy will not broadcast
- // ACTION_SIM_STATE_CHANGED intents
private boolean mInitialized = false;
private State mExternalState = State.UNKNOWN;
@@ -121,8 +114,6 @@ public class IccCardProxy extends Handler implements IccCard {
mPhoneId = phoneId;
mTelephonyManager = (TelephonyManager) mContext.getSystemService(
Context.TELEPHONY_SERVICE);
- mCdmaSSM = CdmaSubscriptionSourceManager.getInstance(context,
- ci, this, EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null);
mUiccController = UiccController.getInstance();
mUiccController.registerForIccChanged(this, EVENT_ICC_CHANGED, null);
ci.registerForOn(this,EVENT_RADIO_ON, null);
@@ -139,7 +130,6 @@ public class IccCardProxy extends Handler implements IccCard {
mUiccController = null;
mCi.unregisterForOn(this);
mCi.unregisterForOffOrNotAvailable(this);
- mCdmaSSM.dispose(this);
}
}
@@ -157,64 +147,31 @@ public class IccCardProxy extends Handler implements IccCard {
} else {
mCurrentAppType = UiccController.APP_FAM_3GPP2;
}
- updateQuietMode();
+ updateCurrentAppType();
}
}
/**
- * In case of 3gpp2 we need to find out if subscription used is coming from
- * NV in which case we shouldn't broadcast any sim states changes.
+ * Update current app type and post EVENT_ICC_CHANGED.
*/
- private void updateQuietMode() {
+ private void updateCurrentAppType() {
synchronized (mLock) {
- boolean oldQuietMode = mQuietMode;
- boolean newQuietMode;
- int cdmaSource = Phone.CDMA_SUBSCRIPTION_UNKNOWN;
boolean isLteOnCdmaMode = TelephonyManager.getLteOnCdmaModeStatic()
== PhoneConstants.LTE_ON_CDMA_TRUE;
- if (mCurrentAppType == UiccController.APP_FAM_3GPP) {
- newQuietMode = false;
- if (DBG) log("updateQuietMode: 3GPP subscription -> newQuietMode=" + newQuietMode);
- } else {
+ if (mCurrentAppType == UiccController.APP_FAM_3GPP2) {
if (isLteOnCdmaMode) {
- log("updateQuietMode: is cdma/lte device, force IccCardProxy into 3gpp mode");
+ log("updateCurrentAppType: is cdma/lte device, force IccCardProxy into 3gpp"
+ + " mode");
mCurrentAppType = UiccController.APP_FAM_3GPP;
}
- cdmaSource = mCdmaSSM != null ?
- mCdmaSSM.getCdmaSubscriptionSource() : Phone.CDMA_SUBSCRIPTION_UNKNOWN;
- newQuietMode = (cdmaSource == Phone.CDMA_SUBSCRIPTION_NV)
- && (mCurrentAppType == UiccController.APP_FAM_3GPP2)
- && !isLteOnCdmaMode;
if (DBG) {
- log("updateQuietMode: cdmaSource=" + cdmaSource
+ log("updateCurrentAppType: "
+ " mCurrentAppType=" + mCurrentAppType
- + " isLteOnCdmaMode=" + isLteOnCdmaMode
- + " newQuietMode=" + newQuietMode);
+ + " isLteOnCdmaMode=" + isLteOnCdmaMode);
}
}
- if (mQuietMode == false && newQuietMode == true) {
- // Last thing to do before switching to quiet mode is
- // broadcast ICC_READY
- log("Switching to QuietMode.");
- setExternalState(State.READY);
- mQuietMode = newQuietMode;
- } else if (mQuietMode == true && newQuietMode == false) {
- if (DBG) {
- log("updateQuietMode: Switching out from QuietMode."
- + " Force broadcast of current state=" + mExternalState);
- }
- mQuietMode = newQuietMode;
- setExternalState(mExternalState, true);
- } else {
- if (DBG) log("updateQuietMode: no changes don't setExternalState");
- }
- if (DBG) {
- log("updateQuietMode: QuietMode is " + mQuietMode + " (app_type="
- + mCurrentAppType + " isLteOnCdmaMode=" + isLteOnCdmaMode
- + " cdmaSource=" + cdmaSource + ")");
- }
mInitialized = true;
sendMessage(obtainMessage(EVENT_ICC_CHANGED));
}
@@ -230,9 +187,9 @@ public class IccCardProxy extends Handler implements IccCard {
case EVENT_RADIO_ON:
mRadioState = RadioState.RADIO_ON;
if (!mInitialized) {
- updateQuietMode();
+ updateCurrentAppType();
} else {
- // updateQuietMode() triggers ICC_CHANGED, which eventually
+ // updateCurrentAppType() triggers ICC_CHANGED, which eventually
// calls updateExternalState; thus, we don't need this in the
// above case
updateExternalState();
@@ -244,7 +201,6 @@ public class IccCardProxy extends Handler implements IccCard {
}
break;
case EVENT_ICC_ABSENT:
- mAbsentRegistrants.notifyRegistrants();
setExternalState(State.ABSENT);
break;
case EVENT_ICC_LOCKED:
@@ -286,9 +242,6 @@ public class IccCardProxy extends Handler implements IccCard {
mNetworkLockedRegistrants.notifyRegistrants();
setExternalState(State.NETWORK_LOCKED);
break;
- case EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED:
- updateQuietMode();
- break;
case EVENT_SUBSCRIPTION_ACTIVATED:
log("EVENT_SUBSCRIPTION_ACTIVATED");
onSubscriptionActivated();
@@ -341,6 +294,7 @@ public class IccCardProxy extends Handler implements IccCard {
private void updateIccAvailability() {
synchronized (mLock) {
+ UiccSlot newSlot = mUiccController.getUiccSlotForPhone(mPhoneId);
UiccCard newCard = mUiccController.getUiccCard(mPhoneId);
UiccCardApplication newApp = null;
IccRecords newRecords = null;
@@ -351,9 +305,11 @@ public class IccCardProxy extends Handler implements IccCard {
}
}
- if (mIccRecords != newRecords || mUiccApplication != newApp || mUiccCard != newCard) {
+ if (mIccRecords != newRecords || mUiccApplication != newApp || mUiccCard != newCard
+ || mUiccSlot != newSlot) {
if (DBG) log("Icc changed. Reregistering.");
unregisterUiccCardEvents();
+ mUiccSlot = newSlot;
mUiccCard = newCard;
mUiccApplication = newApp;
mIccRecords = newRecords;
@@ -440,17 +396,6 @@ public class IccCardProxy extends Handler implements IccCard {
case APPSTATE_DETECTED:
HandleDetectedState();
break;
- case APPSTATE_PIN:
- setExternalState(State.PIN_REQUIRED);
- break;
- case APPSTATE_PUK:
- PinState pin1State = mUiccApplication.getPin1State();
- if (pin1State.isPermBlocked()) {
- setExternalState(State.PERM_DISABLED);
- return;
- }
- setExternalState(State.PUK_REQUIRED);
- break;
case APPSTATE_SUBSCRIPTION_PERSO:
if (mUiccApplication.getPersoSubState() ==
PersoSubState.PERSOSUBSTATE_SIM_NETWORK) {
@@ -465,23 +410,24 @@ public class IccCardProxy extends Handler implements IccCard {
}
private void registerUiccCardEvents() {
- if (mUiccCard != null) {
- mUiccCard.registerForAbsent(this, EVENT_ICC_ABSENT, null);
+ if (mUiccSlot != null) {
+ // todo: reregistration is not needed unless slot mapping changes
+ mUiccSlot.registerForAbsent(this, EVENT_ICC_ABSENT, null);
}
if (mUiccApplication != null) {
mUiccApplication.registerForReady(this, EVENT_APP_READY, null);
- mUiccApplication.registerForLocked(this, EVENT_ICC_LOCKED, null);
mUiccApplication.registerForNetworkLocked(this, EVENT_NETWORK_LOCKED, null);
}
if (mIccRecords != null) {
mIccRecords.registerForImsiReady(this, EVENT_IMSI_READY, null);
mIccRecords.registerForRecordsLoaded(this, EVENT_RECORDS_LOADED, null);
+ mIccRecords.registerForLockedRecordsLoaded(this, EVENT_ICC_LOCKED, null);
mIccRecords.registerForRecordsEvents(this, EVENT_ICC_RECORD_EVENTS, null);
}
}
private void unregisterUiccCardEvents() {
- if (mUiccCard != null) mUiccCard.unregisterForAbsent(this);
+ if (mUiccSlot != null) mUiccSlot.unregisterForAbsent(this);
if (mUiccCard != null) mUiccCard.unregisterForCarrierPrivilegeRulesLoaded(this);
if (mUiccApplication != null) mUiccApplication.unregisterForReady(this);
if (mUiccApplication != null) mUiccApplication.unregisterForLocked(this);
@@ -503,13 +449,6 @@ public class IccCardProxy extends Handler implements IccCard {
return;
}
- if (mQuietMode) {
- log("broadcastIccStateChangedIntent: QuietMode"
- + " NOT Broadcasting intent ACTION_SIM_STATE_CHANGED "
- + " value=" + value + " reason=" + reason);
- return;
- }
-
Intent intent = new Intent(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
// TODO - we'd like this intent to have a single snapshot of all sim state,
// but until then this should not use REPLACE_PENDING or we may lose
@@ -570,10 +509,6 @@ public class IccCardProxy extends Handler implements IccCard {
broadcastIccStateChangedIntent(getIccStateIntentString(mExternalState),
getIccStateReason(mExternalState));
}
- // TODO: Need to notify registrants for other states as well.
- if ( State.ABSENT == mExternalState) {
- mAbsentRegistrants.notifyRegistrants();
- }
}
}
@@ -592,7 +527,6 @@ public class IccCardProxy extends Handler implements IccCard {
AppState appState = mUiccApplication.getState();
switch (appState) {
case APPSTATE_PIN:
- mPinLockedRegistrants.notifyRegistrants();
setExternalState(State.PIN_REQUIRED);
break;
case APPSTATE_PUK:
@@ -678,29 +612,6 @@ public class IccCardProxy extends Handler implements IccCard {
}
/**
- * Notifies handler of any transition into State.ABSENT
- */
- @Override
- public void registerForAbsent(Handler h, int what, Object obj) {
- synchronized (mLock) {
- Registrant r = new Registrant (h, what, obj);
-
- mAbsentRegistrants.add(r);
-
- if (getState() == State.ABSENT) {
- r.notifyRegistrant();
- }
- }
- }
-
- @Override
- public void unregisterForAbsent(Handler h) {
- synchronized (mLock) {
- mAbsentRegistrants.remove(h);
- }
- }
-
- /**
* Notifies handler of any transition into State.NETWORK_LOCKED
*/
@Override
@@ -723,29 +634,6 @@ public class IccCardProxy extends Handler implements IccCard {
}
}
- /**
- * Notifies handler of any transition into State.isPinLocked()
- */
- @Override
- public void registerForLocked(Handler h, int what, Object obj) {
- synchronized (mLock) {
- Registrant r = new Registrant (h, what, obj);
-
- mPinLockedRegistrants.add(r);
-
- if (getState().isPinLocked()) {
- r.notifyRegistrant();
- }
- }
- }
-
- @Override
- public void unregisterForLocked(Handler h) {
- synchronized (mLock) {
- mPinLockedRegistrants.remove(h);
- }
- }
-
@Override
public void supplyPin(String pin, Message onComplete) {
synchronized (mLock) {
@@ -936,13 +824,6 @@ public class IccCardProxy extends Handler implements IccCard {
}
}
- private void setSystemProperty(String property, String value) {
- TelephonyManager.setTelephonyProperty(mPhoneId, property, value);
- }
-
- public IccRecords getIccRecord() {
- return mIccRecords;
- }
private void log(String s) {
Rlog.d(LOG_TAG, s);
}
@@ -955,16 +836,6 @@ public class IccCardProxy extends Handler implements IccCard {
pw.println("IccCardProxy: " + this);
pw.println(" mContext=" + mContext);
pw.println(" mCi=" + mCi);
- pw.println(" mAbsentRegistrants: size=" + mAbsentRegistrants.size());
- for (int i = 0; i < mAbsentRegistrants.size(); i++) {
- pw.println(" mAbsentRegistrants[" + i + "]="
- + ((Registrant)mAbsentRegistrants.get(i)).getHandler());
- }
- pw.println(" mPinLockedRegistrants: size=" + mPinLockedRegistrants.size());
- for (int i = 0; i < mPinLockedRegistrants.size(); i++) {
- pw.println(" mPinLockedRegistrants[" + i + "]="
- + ((Registrant)mPinLockedRegistrants.get(i)).getHandler());
- }
pw.println(" mNetworkLockedRegistrants: size=" + mNetworkLockedRegistrants.size());
for (int i = 0; i < mNetworkLockedRegistrants.size(); i++) {
pw.println(" mNetworkLockedRegistrants[" + i + "]="
@@ -975,9 +846,7 @@ public class IccCardProxy extends Handler implements IccCard {
pw.println(" mUiccCard=" + mUiccCard);
pw.println(" mUiccApplication=" + mUiccApplication);
pw.println(" mIccRecords=" + mIccRecords);
- pw.println(" mCdmaSSM=" + mCdmaSSM);
pw.println(" mRadioState=" + mRadioState);
- pw.println(" mQuietMode=" + mQuietMode);
pw.println(" mInitialized=" + mInitialized);
pw.println(" mExternalState=" + mExternalState);
diff --git a/com/android/internal/telephony/uicc/IccCardStatus.java b/com/android/internal/telephony/uicc/IccCardStatus.java
index f14f21d2..45e85c23 100644
--- a/com/android/internal/telephony/uicc/IccCardStatus.java
+++ b/com/android/internal/telephony/uicc/IccCardStatus.java
@@ -16,6 +16,8 @@
package com.android.internal.telephony.uicc;
+import android.telephony.SubscriptionInfo;
+
/**
* See also RIL_CardStatus in include/telephony/ril.h
*
@@ -62,6 +64,9 @@ public class IccCardStatus {
public int mGsmUmtsSubscriptionAppIndex;
public int mCdmaSubscriptionAppIndex;
public int mImsSubscriptionAppIndex;
+ public int physicalSlotIndex;
+ public String atr;
+ public String iccid;
public IccCardApplicationStatus[] mApplications;
@@ -142,6 +147,9 @@ public class IccCardStatus {
sb.append(app == null ? "null" : app);
}
+ sb.append(",physical_slot_id=").append(physicalSlotIndex).append(",atr=").append(atr);
+ sb.append(",iccid=").append(SubscriptionInfo.givePrintableIccid(iccid));
+
sb.append("}");
return sb.toString();
}
diff --git a/com/android/internal/telephony/uicc/IccRecords.java b/com/android/internal/telephony/uicc/IccRecords.java
index 4e4158a1..b37467e3 100644
--- a/com/android/internal/telephony/uicc/IccRecords.java
+++ b/com/android/internal/telephony/uicc/IccRecords.java
@@ -55,6 +55,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
protected TelephonyManager mTelephonyManager;
protected RegistrantList mRecordsLoadedRegistrants = new RegistrantList();
+ protected RegistrantList mLockedRecordsLoadedRegistrants = new RegistrantList();
protected RegistrantList mImsiReadyRegistrants = new RegistrantList();
protected RegistrantList mRecordsEventsRegistrants = new RegistrantList();
protected RegistrantList mNewSmsRegistrants = new RegistrantList();
@@ -68,8 +69,12 @@ public abstract class IccRecords extends Handler implements IccConstants {
// ***** Cached SIM State; cleared on channel close
protected boolean mRecordsRequested = false; // true if we've made requests for the sim records
+ protected boolean mLockedRecordsRequested = false; // true if parent app is locked and we've
+ // made requests for the sim records
protected String mIccId; // Includes only decimals (no hex)
+ protected String mFakeIccId;
+
protected String mFullIccId; // Includes hex characters in ICCID
protected String mMsisdn = null; // My mobile number
protected String mMsisdnTag = null;
@@ -155,7 +160,9 @@ public abstract class IccRecords extends Handler implements IccConstants {
+ " recordsToLoad=" + mRecordsToLoad
+ " adnCache=" + mAdnCache
+ " recordsRequested=" + mRecordsRequested
+ + " lockedRecordsRequested=" + mLockedRecordsRequested
+ " iccid=" + iccIdToPrint
+ + (mCarrierTestOverride.isInTestMode() ? "mFakeIccid=" + mFakeIccId : "")
+ " msisdnTag=" + mMsisdnTag
+ " voiceMailNum=" + Rlog.pii(VDBG, mVoiceMailNum)
+ " voiceMailTag=" + mVoiceMailTag
@@ -169,7 +176,6 @@ public abstract class IccRecords extends Handler implements IccConstants {
+ " mailboxIndex=" + mMailboxIndex
+ " spn=" + mSpn
+ (mCarrierTestOverride.isInTestMode() ? " mFakeSpn=" + mFakeSpn : "");
-
}
/**
@@ -214,6 +220,9 @@ public abstract class IccRecords extends Handler implements IccConstants {
mFakePnnHomeName = mCarrierTestOverride.getFakePnnHomeName();
log("load mFakePnnHomeName: " + mFakePnnHomeName);
+
+ mFakeIccId = mCarrierTestOverride.getFakeIccid();
+ log("load mFakeIccId: " + mFakeIccId);
}
}
@@ -272,7 +281,11 @@ public abstract class IccRecords extends Handler implements IccConstants {
* @return ICC ID without hex digits
*/
public String getIccId() {
- return mIccId;
+ if (mCarrierTestOverride.isInTestMode() && mFakeIccId != null) {
+ return mFakeIccId;
+ } else {
+ return mIccId;
+ }
}
/**
@@ -291,7 +304,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
Registrant r = new Registrant(h, what, obj);
mRecordsLoadedRegistrants.add(r);
- if (mRecordsToLoad == 0 && mRecordsRequested == true) {
+ if (getRecordsLoaded()) {
r.notifyRegistrant(new AsyncResult(null, null, null));
}
}
@@ -299,6 +312,29 @@ public abstract class IccRecords extends Handler implements IccConstants {
mRecordsLoadedRegistrants.remove(h);
}
+ /**
+ * Register to be notified when records are loaded for a locked SIM
+ */
+ public void registerForLockedRecordsLoaded(Handler h, int what, Object obj) {
+ if (mDestroyed.get()) {
+ return;
+ }
+
+ Registrant r = new Registrant(h, what, obj);
+ mLockedRecordsLoadedRegistrants.add(r);
+
+ if (getLockedRecordsLoaded()) {
+ r.notifyRegistrant(new AsyncResult(null, null, null));
+ }
+ }
+
+ /**
+ * Unregister corresponding to registerForLockedRecordsLoaded()
+ */
+ public void unregisterForLockedRecordsLoaded(Handler h) {
+ mLockedRecordsLoadedRegistrants.remove(h);
+ }
+
public void registerForImsiReady(Handler h, int what, Object obj) {
if (mDestroyed.get()) {
return;
@@ -560,11 +596,11 @@ public abstract class IccRecords extends Handler implements IccConstants {
}
public boolean getRecordsLoaded() {
- if (mRecordsToLoad == 0 && mRecordsRequested == true) {
- return true;
- } else {
- return false;
- }
+ return mRecordsToLoad == 0 && mRecordsRequested;
+ }
+
+ protected boolean getLockedRecordsLoaded() {
+ return mRecordsToLoad == 0 && mLockedRecordsRequested;
}
//***** Overridden from Handler
@@ -822,6 +858,12 @@ public abstract class IccRecords extends Handler implements IccConstants {
pw.println(" recordsLoadedRegistrants[" + i + "]="
+ ((Registrant)mRecordsLoadedRegistrants.get(i)).getHandler());
}
+ pw.println(" mLockedRecordsLoadedRegistrants: size="
+ + mLockedRecordsLoadedRegistrants.size());
+ for (int i = 0; i < mLockedRecordsLoadedRegistrants.size(); i++) {
+ pw.println(" mLockedRecordsLoadedRegistrants[" + i + "]="
+ + ((Registrant) mLockedRecordsLoadedRegistrants.get(i)).getHandler());
+ }
pw.println(" mImsiReadyRegistrants: size=" + mImsiReadyRegistrants.size());
for (int i = 0; i < mImsiReadyRegistrants.size(); i++) {
pw.println(" mImsiReadyRegistrants[" + i + "]="
@@ -844,6 +886,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
+ ((Registrant)mNetworkSelectionModeAutomaticRegistrants.get(i)).getHandler());
}
pw.println(" mRecordsRequested=" + mRecordsRequested);
+ pw.println(" mLockedRecordsRequested=" + mLockedRecordsRequested);
pw.println(" mRecordsToLoad=" + mRecordsToLoad);
pw.println(" mRdnCache=" + mAdnCache);
diff --git a/com/android/internal/telephony/uicc/IccSlotStatus.java b/com/android/internal/telephony/uicc/IccSlotStatus.java
new file mode 100644
index 00000000..f9e87c71
--- /dev/null
+++ b/com/android/internal/telephony/uicc/IccSlotStatus.java
@@ -0,0 +1,88 @@
+/*
+ * 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 com.android.internal.telephony.uicc;
+
+import android.telephony.SubscriptionInfo;
+
+/**
+ * This class represents the status of the physical UICC slots.
+ */
+public class IccSlotStatus {
+
+ public enum SlotState {
+ SLOTSTATE_INACTIVE,
+ SLOTSTATE_ACTIVE;
+ }
+
+ public IccCardStatus.CardState cardState;
+ public SlotState slotState;
+ public int logicalSlotIndex;
+ public String atr;
+ public String iccid;
+
+ /**
+ * Set the cardState according to the input state.
+ */
+ public void setCardState(int state) {
+ switch(state) {
+ case 0:
+ cardState = IccCardStatus.CardState.CARDSTATE_ABSENT;
+ break;
+ case 1:
+ cardState = IccCardStatus.CardState.CARDSTATE_PRESENT;
+ break;
+ case 2:
+ cardState = IccCardStatus.CardState.CARDSTATE_ERROR;
+ break;
+ case 3:
+ cardState = IccCardStatus.CardState.CARDSTATE_RESTRICTED;
+ break;
+ default:
+ throw new RuntimeException("Unrecognized RIL_CardState: " + state);
+ }
+ }
+
+ /**
+ * Set the slotState according to the input state.
+ */
+ public void setSlotState(int state) {
+ switch(state) {
+ case 0:
+ slotState = SlotState.SLOTSTATE_INACTIVE;
+ break;
+ case 1:
+ slotState = SlotState.SLOTSTATE_ACTIVE;
+ break;
+ default:
+ throw new RuntimeException("Unrecognized RIL_SlotState: " + state);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("IccSlotStatus {").append(cardState).append(",")
+ .append(slotState).append(",")
+ .append("logicalSlotIndex=").append(logicalSlotIndex).append(",")
+ .append("atr=").append(atr).append(",iccid=")
+ .append(SubscriptionInfo.givePrintableIccid(iccid));
+
+ sb.append("}");
+ return sb.toString();
+ }
+
+}
diff --git a/com/android/internal/telephony/uicc/IccUtils.java b/com/android/internal/telephony/uicc/IccUtils.java
index 99a82ad0..9f8b3a82 100644
--- a/com/android/internal/telephony/uicc/IccUtils.java
+++ b/com/android/internal/telephony/uicc/IccUtils.java
@@ -32,6 +32,12 @@ import java.io.UnsupportedEncodingException;
public class IccUtils {
static final String LOG_TAG="IccUtils";
+ // A table mapping from a number to a hex character for fast encoding hex strings.
+ private static final char[] HEX_CHARS = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+
+
/**
* Many fields in GSM SIM's are stored as nibble-swizzled BCD
*
@@ -62,6 +68,41 @@ public class IccUtils {
}
/**
+ * Converts a bcd byte array to String with offset 0 and byte array length.
+ */
+ public static String bcdToString(byte[] data) {
+ return bcdToString(data, 0, data.length);
+ }
+
+ /**
+ * Converts BCD string to bytes.
+ *
+ * @param bcd This should have an even length. If not, an "0" will be appended to the string.
+ */
+ public static byte[] bcdToBytes(String bcd) {
+ byte[] output = new byte[(bcd.length() + 1) / 2];
+ bcdToBytes(bcd, output);
+ return output;
+ }
+
+ /**
+ * Converts BCD string to bytes and put it into the given byte array.
+ *
+ * @param bcd This should have an even length. If not, an "0" will be appended to the string.
+ * @param bytes If the array size is less than needed, the rest of the BCD string isn't be
+ * converted. If the array size is more than needed, the rest of array remains unchanged.
+ */
+ public static void bcdToBytes(String bcd, byte[] bytes) {
+ if (bcd.length() % 2 != 0) {
+ bcd += "0";
+ }
+ int size = Math.min(bytes.length * 2, bcd.length());
+ for (int i = 0, j = 0; i + 1 < size; i += 2, j++) {
+ bytes[j] = (byte) (charToByte(bcd.charAt(i + 1)) << 4 | charToByte(bcd.charAt(i)));
+ }
+ }
+
+ /**
* PLMN (MCC/MNC) is encoded as per 24.008 10.5.1.3
* Returns a concatenated string of MCC+MNC, stripping
* all invalid character 'f'
@@ -94,10 +135,10 @@ public class IccUtils {
int v;
v = data[i] & 0xf;
- ret.append("0123456789abcdef".charAt(v));
+ ret.append(HEX_CHARS[v]);
v = (data[i] >> 4) & 0xf;
- ret.append("0123456789abcdef".charAt(v));
+ ret.append(HEX_CHARS[v]);
}
return ret.toString();
@@ -305,7 +346,7 @@ public class IccUtils {
return GsmAlphabet.gsm8BitUnpackedToString(data, offset, length, defaultCharset.trim());
}
- static int
+ public static int
hexCharToInt(char c) {
if (c >= '0' && c <= '9') return (c - '0');
if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
@@ -361,11 +402,11 @@ public class IccUtils {
b = 0x0f & (bytes[i] >> 4);
- ret.append("0123456789abcdef".charAt(b));
+ ret.append(HEX_CHARS[b]);
b = 0x0f & bytes[i];
- ret.append("0123456789abcdef".charAt(b));
+ ret.append(HEX_CHARS[b]);
}
return ret.toString();
@@ -416,7 +457,6 @@ public class IccUtils {
if ((data[offset] & 0x40) != 0) {
// FIXME(mkf) add country initials here
-
}
return ret;
@@ -575,4 +615,239 @@ public class IccUtils {
}
return iccId.substring( 0, position );
}
+
+ /**
+ * Converts a series of bytes to an integer. This method currently only supports positive 32-bit
+ * integers.
+ *
+ * @param src The source bytes.
+ * @param offset The position of the first byte of the data to be converted. The data is base
+ * 256 with the most significant digit first.
+ * @param length The length of the data to be converted. It must be <= 4.
+ * @throws IllegalArgumentException If {@code length} is bigger than 4 or {@code src} cannot be
+ * parsed as a positive integer.
+ * @throws IndexOutOfBoundsException If the range defined by {@code offset} and {@code length}
+ * exceeds the bounds of {@code src}.
+ */
+ public static int bytesToInt(byte[] src, int offset, int length) {
+ if (length > 4) {
+ throw new IllegalArgumentException(
+ "length must be <= 4 (only 32-bit integer supported): " + length);
+ }
+ if (offset < 0 || length < 0 || offset + length > src.length) {
+ throw new IndexOutOfBoundsException(
+ "Out of the bounds: src=["
+ + src.length
+ + "], offset="
+ + offset
+ + ", length="
+ + length);
+ }
+ int result = 0;
+ for (int i = 0; i < length; i++) {
+ result = (result << 8) | (src[offset + i] & 0xFF);
+ }
+ if (result < 0) {
+ throw new IllegalArgumentException(
+ "src cannot be parsed as a positive integer: " + result);
+ }
+ return result;
+ }
+
+ /**
+ * Converts a series of bytes to a raw long variable which can be both positive and negative.
+ * This method currently only supports 64-bit long variable.
+ *
+ * @param src The source bytes.
+ * @param offset The position of the first byte of the data to be converted. The data is base
+ * 256 with the most significant digit first.
+ * @param length The length of the data to be converted. It must be <= 8.
+ * @throws IllegalArgumentException If {@code length} is bigger than 8.
+ * @throws IndexOutOfBoundsException If the range defined by {@code offset} and {@code length}
+ * exceeds the bounds of {@code src}.
+ */
+ public static long bytesToRawLong(byte[] src, int offset, int length) {
+ if (length > 8) {
+ throw new IllegalArgumentException(
+ "length must be <= 8 (only 64-bit long supported): " + length);
+ }
+ if (offset < 0 || length < 0 || offset + length > src.length) {
+ throw new IndexOutOfBoundsException(
+ "Out of the bounds: src=["
+ + src.length
+ + "], offset="
+ + offset
+ + ", length="
+ + length);
+ }
+ long result = 0;
+ for (int i = 0; i < length; i++) {
+ result = (result << 8) | (src[offset + i] & 0xFF);
+ }
+ return result;
+ }
+
+ /**
+ * Converts an integer to a new byte array with base 256 and the most significant digit first.
+ *
+ * @throws IllegalArgumentException If {@code value} is negative.
+ */
+ public static byte[] unsignedIntToBytes(int value) {
+ if (value < 0) {
+ throw new IllegalArgumentException("value must be 0 or positive: " + value);
+ }
+ byte[] bytes = new byte[byteNumForUnsignedInt(value)];
+ unsignedIntToBytes(value, bytes, 0);
+ return bytes;
+ }
+
+ /**
+ * Converts an integer to a new byte array with base 256 and the most significant digit first.
+ * The first byte's highest bit is used for sign. If the most significant digit is larger than
+ * 127, an extra byte (0) will be prepended before it. This method currently doesn't support
+ * negative values.
+ *
+ * @throws IllegalArgumentException If {@code value} is negative.
+ */
+ public static byte[] signedIntToBytes(int value) {
+ if (value < 0) {
+ throw new IllegalArgumentException("value must be 0 or positive: " + value);
+ }
+ byte[] bytes = new byte[byteNumForSignedInt(value)];
+ signedIntToBytes(value, bytes, 0);
+ return bytes;
+ }
+
+ /**
+ * Converts an integer to a series of bytes with base 256 and the most significant digit first.
+ *
+ * @param value The integer to be converted.
+ * @param dest The destination byte array.
+ * @param offset The start offset of the byte array.
+ * @return The number of byte needeed.
+ * @throws IllegalArgumentException If {@code value} is negative.
+ * @throws IndexOutOfBoundsException If {@code offset} exceeds the bounds of {@code dest}.
+ */
+ public static int unsignedIntToBytes(int value, byte[] dest, int offset) {
+ return intToBytes(value, dest, offset, false);
+ }
+
+ /**
+ * Converts an integer to a series of bytes with base 256 and the most significant digit first.
+ * The first byte's highest bit is used for sign. If the most significant digit is larger than
+ * 127, an extra byte (0) will be prepended before it. This method currently doesn't support
+ * negative values.
+ *
+ * @throws IllegalArgumentException If {@code value} is negative.
+ * @throws IndexOutOfBoundsException If {@code offset} exceeds the bounds of {@code dest}.
+ */
+ public static int signedIntToBytes(int value, byte[] dest, int offset) {
+ return intToBytes(value, dest, offset, true);
+ }
+
+ /**
+ * Calculates the number of required bytes to represent {@code value}. The bytes will be base
+ * 256 with the most significant digit first.
+ *
+ * @throws IllegalArgumentException If {@code value} is negative.
+ */
+ public static int byteNumForUnsignedInt(int value) {
+ return byteNumForInt(value, false);
+ }
+
+ /**
+ * Calculates the number of required bytes to represent {@code value}. The bytes will be base
+ * 256 with the most significant digit first. If the most significant digit is larger than 127,
+ * an extra byte (0) will be prepended before it. This method currently only supports positive
+ * integers.
+ *
+ * @throws IllegalArgumentException If {@code value} is negative.
+ */
+ public static int byteNumForSignedInt(int value) {
+ return byteNumForInt(value, true);
+ }
+
+ private static int intToBytes(int value, byte[] dest, int offset, boolean signed) {
+ int l = byteNumForInt(value, signed);
+ if (offset < 0 || offset + l > dest.length) {
+ throw new IndexOutOfBoundsException("Not enough space to write. Required bytes: " + l);
+ }
+ for (int i = l - 1, v = value; i >= 0; i--, v >>>= 8) {
+ byte b = (byte) (v & 0xFF);
+ dest[offset + i] = b;
+ }
+ return l;
+ }
+
+ private static int byteNumForInt(int value, boolean signed) {
+ if (value < 0) {
+ throw new IllegalArgumentException("value must be 0 or positive: " + value);
+ }
+ if (signed) {
+ if (value <= 0x7F) {
+ return 1;
+ }
+ if (value <= 0x7FFF) {
+ return 2;
+ }
+ if (value <= 0x7FFFFF) {
+ return 3;
+ }
+ } else {
+ if (value <= 0xFF) {
+ return 1;
+ }
+ if (value <= 0xFFFF) {
+ return 2;
+ }
+ if (value <= 0xFFFFFF) {
+ return 3;
+ }
+ }
+ return 4;
+ }
+
+
+ /**
+ * Counts the number of trailing zero bits of a byte.
+ */
+ public static byte countTrailingZeros(byte b) {
+ if (b == 0) {
+ return 8;
+ }
+ int v = b & 0xFF;
+ byte c = 7;
+ if ((v & 0x0F) != 0) {
+ c -= 4;
+ }
+ if ((v & 0x33) != 0) {
+ c -= 2;
+ }
+ if ((v & 0x55) != 0) {
+ c -= 1;
+ }
+ return c;
+ }
+
+ /**
+ * Converts a byte to a hex string.
+ */
+ public static String byteToHex(byte b) {
+ return new String(new char[] {HEX_CHARS[(b & 0xFF) >>> 4], HEX_CHARS[b & 0xF]});
+ }
+
+ /**
+ * Converts a character of [0-9a-aA-F] to its hex value in a byte. If the character is not a
+ * hex number, 0 will be returned.
+ */
+ private static byte charToByte(char c) {
+ if (c >= 0x30 && c <= 0x39) {
+ return (byte) (c - 0x30);
+ } else if (c >= 0x41 && c <= 0x46) {
+ return (byte) (c - 0x37);
+ } else if (c >= 0x61 && c <= 0x66) {
+ return (byte) (c - 0x57);
+ }
+ return 0;
+ }
}
diff --git a/com/android/internal/telephony/uicc/IsimUiccRecords.java b/com/android/internal/telephony/uicc/IsimUiccRecords.java
index 3d1fafb0..cc9f3a32 100644
--- a/com/android/internal/telephony/uicc/IsimUiccRecords.java
+++ b/com/android/internal/telephony/uicc/IsimUiccRecords.java
@@ -82,6 +82,9 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
super(app, c, ci);
mRecordsRequested = false; // No load request is made till SIM ready
+ //todo: currently locked state for ISIM is not handled well and may cause app state to not
+ //be broadcast
+ mLockedRecordsRequested = false;
// recordsToLoad is set to 0 because no requests are made yet
mRecordsToLoad = 0;
@@ -196,6 +199,7 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
auth_rsp = null;
mRecordsRequested = false;
+ mLockedRecordsRequested = false;
}
private class EfIsimImpiLoaded implements IccRecords.IccRecordLoaded {
@@ -291,19 +295,25 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
mRecordsToLoad -= 1;
if (DBG) log("onRecordLoaded " + mRecordsToLoad + " requested: " + mRecordsRequested);
- if (mRecordsToLoad == 0 && mRecordsRequested == true) {
+ if (getRecordsLoaded()) {
onAllRecordsLoaded();
+ } else if (getLockedRecordsLoaded()) {
+ onLockedAllRecordsLoaded();
} else if (mRecordsToLoad < 0) {
loge("recordsToLoad <0, programmer error suspected");
mRecordsToLoad = 0;
}
}
+ private void onLockedAllRecordsLoaded() {
+ if (DBG) log("SIM locked; record load complete");
+ mLockedRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
+ }
+
@Override
protected void onAllRecordsLoaded() {
if (DBG) log("record load complete");
- mRecordsLoadedRegistrants.notifyRegistrants(
- new AsyncResult(null, null, null));
+ mRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
}
private void handleFileUpdate(int efid) {
diff --git a/com/android/internal/telephony/uicc/RuimRecords.java b/com/android/internal/telephony/uicc/RuimRecords.java
index 5cc343e6..b57d8fcb 100644
--- a/com/android/internal/telephony/uicc/RuimRecords.java
+++ b/com/android/internal/telephony/uicc/RuimRecords.java
@@ -98,6 +98,7 @@ public class RuimRecords extends IccRecords {
private static final int EVENT_GET_SMS_DONE = 22;
private static final int EVENT_RUIM_REFRESH = 31;
+ private static final int EVENT_APP_LOCKED = 32;
public RuimRecords(UiccCardApplication app, Context c, CommandsInterface ci) {
super(app, c, ci);
@@ -105,6 +106,7 @@ public class RuimRecords extends IccRecords {
mAdnCache = new AdnRecordCache(mFh);
mRecordsRequested = false; // No load request is made till SIM ready
+ mLockedRecordsRequested = false;
// recordsToLoad is set to 0 because no requests are made yet
mRecordsToLoad = 0;
@@ -116,6 +118,7 @@ public class RuimRecords extends IccRecords {
resetRecords();
mParentApp.registerForReady(this, EVENT_APP_READY, null);
+ mParentApp.registerForLocked(this, EVENT_APP_LOCKED, null);
if (DBG) log("RuimRecords X ctor this=" + this);
}
@@ -152,6 +155,7 @@ public class RuimRecords extends IccRecords {
// read requests made so far are not valid. This is set to
// true only when fresh set of read requests are made.
mRecordsRequested = false;
+ mLockedRecordsRequested = false;
}
public String getMdnNumber() {
@@ -597,11 +601,16 @@ public class RuimRecords extends IccRecords {
return;
}
- try { switch (msg.what) {
+ try {
+ switch (msg.what) {
case EVENT_APP_READY:
onReady();
break;
+ case EVENT_APP_LOCKED:
+ onLocked();
+ break;
+
case EVENT_GET_DEVICE_IDENTITY_DONE:
log("Event EVENT_GET_DEVICE_IDENTITY_DONE Received");
break;
@@ -745,14 +754,20 @@ public class RuimRecords extends IccRecords {
mRecordsToLoad -= 1;
if (DBG) log("onRecordLoaded " + mRecordsToLoad + " requested: " + mRecordsRequested);
- if (mRecordsToLoad == 0 && mRecordsRequested == true) {
+ if (getRecordsLoaded()) {
onAllRecordsLoaded();
+ } else if (getLockedRecordsLoaded()) {
+ onLockedAllRecordsLoaded();
} else if (mRecordsToLoad < 0) {
loge("recordsToLoad <0, programmer error suspected");
mRecordsToLoad = 0;
}
}
+ private void onLockedAllRecordsLoaded() {
+ mLockedRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
+ }
+
@Override
protected void onAllRecordsLoaded() {
if (DBG) log("record load complete");
@@ -790,8 +805,7 @@ public class RuimRecords extends IccRecords {
setSimLanguage(mEFli, mEFpl);
}
- mRecordsLoadedRegistrants.notifyRegistrants(
- new AsyncResult(null, null, null));
+ mRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
// TODO: The below is hacky since the SubscriptionController may not be ready at this time.
if (!TextUtils.isEmpty(mMdn)) {
@@ -812,6 +826,13 @@ public class RuimRecords extends IccRecords {
mCi.getCDMASubscription(obtainMessage(EVENT_GET_CDMA_SUBSCRIPTION_DONE));
}
+ private void onLocked() {
+ if (DBG) log("only fetch EF_ICCID in locked state");
+ mLockedRecordsRequested = true;
+
+ mFh.loadEFTransparent(EF_ICCID, obtainMessage(EVENT_GET_ICCID_DONE));
+ mRecordsToLoad++;
+ }
private void fetchRuimRecords() {
mRecordsRequested = true;
diff --git a/com/android/internal/telephony/uicc/SIMRecords.java b/com/android/internal/telephony/uicc/SIMRecords.java
index 3af04a9e..4c8f4eeb 100644
--- a/com/android/internal/telephony/uicc/SIMRecords.java
+++ b/com/android/internal/telephony/uicc/SIMRecords.java
@@ -38,7 +38,6 @@ import com.android.internal.telephony.MccTable;
import com.android.internal.telephony.SmsConstants;
import com.android.internal.telephony.SubscriptionController;
import com.android.internal.telephony.gsm.SimTlv;
-import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
import java.io.FileDescriptor;
@@ -212,6 +211,7 @@ public class SIMRecords extends IccRecords {
mVmConfig = new VoiceMailConstants();
mRecordsRequested = false; // No load request is made till SIM ready
+ mLockedRecordsRequested = false;
// recordsToLoad is set to 0 because no requests are made yet
mRecordsToLoad = 0;
@@ -291,6 +291,7 @@ public class SIMRecords extends IccRecords {
// read requests made so far are not valid. This is set to
// true only when fresh set of read requests are made.
mRecordsRequested = false;
+ mLockedRecordsRequested = false;
}
//***** Public Methods
@@ -917,7 +918,6 @@ public class SIMRecords extends IccRecords {
log("iccid: " + SubscriptionInfo.givePrintableIccid(mFullIccId));
break;
-
case EVENT_GET_AD_DONE:
try {
isRecordLoadResponse = true;
@@ -1559,8 +1559,10 @@ public class SIMRecords extends IccRecords {
mRecordsToLoad -= 1;
if (DBG) log("onRecordLoaded " + mRecordsToLoad + " requested: " + mRecordsRequested);
- if (mRecordsToLoad == 0 && mRecordsRequested == true) {
+ if (getRecordsLoaded()) {
onAllRecordsLoaded();
+ } else if (getLockedRecordsLoaded()) {
+ onLockedAllRecordsLoaded();
} else if (mRecordsToLoad < 0) {
loge("recordsToLoad <0, programmer error suspected");
mRecordsToLoad = 0;
@@ -1583,26 +1585,26 @@ public class SIMRecords extends IccRecords {
}
}
- @Override
- protected void onAllRecordsLoaded() {
- if (DBG) log("record load complete");
-
+ private void setSimLanguageFromEF() {
Resources resource = Resources.getSystem();
if (resource.getBoolean(com.android.internal.R.bool.config_use_sim_language_file)) {
setSimLanguage(mEfLi, mEfPl);
} else {
if (DBG) log ("Not using EF LI/EF PL");
}
+ }
- setVoiceCallForwardingFlagFromSimRecords();
+ private void onLockedAllRecordsLoaded() {
+ setSimLanguageFromEF();
+ mLockedRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
+ }
- if (mParentApp.getState() == AppState.APPSTATE_PIN ||
- mParentApp.getState() == AppState.APPSTATE_PUK) {
- // reset recordsRequested, since sim is not loaded really
- mRecordsRequested = false;
- // lock state, only update language
- return ;
- }
+ @Override
+ protected void onAllRecordsLoaded() {
+ if (DBG) log("record load complete");
+
+ setSimLanguageFromEF();
+ setVoiceCallForwardingFlagFromSimRecords();
// Some fields require more than one SIM record to set
@@ -1629,8 +1631,7 @@ public class SIMRecords extends IccRecords {
setVoiceMailByCountry(operator);
- mRecordsLoadedRegistrants.notifyRegistrants(
- new AsyncResult(null, null, null));
+ mRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
}
//***** Private methods
@@ -1716,13 +1717,17 @@ public class SIMRecords extends IccRecords {
}
private void onLocked() {
- if (DBG) log("only fetch EF_LI and EF_PL in lock state");
+ if (DBG) log("only fetch EF_LI, EF_PL and EF_ICCID in locked state");
+ mLockedRecordsRequested = true;
+
loadEfLiAndEfPl();
+
+ mFh.loadEFTransparent(EF_ICCID, obtainMessage(EVENT_GET_ICCID_DONE));
+ mRecordsToLoad++;
}
private void loadEfLiAndEfPl() {
if (mParentApp.getType() == AppType.APPTYPE_USIM) {
- mRecordsRequested = true;
mFh.loadEFTransparent(EF_LI,
obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfUsimLiLoaded()));
mRecordsToLoad++;
@@ -2234,7 +2239,6 @@ public class SIMRecords extends IccRecords {
pw.println(" mEfCfis[]=" + Arrays.toString(mEfCfis));
pw.println(" mSpnDisplayCondition=" + mSpnDisplayCondition);
pw.println(" mSpdiNetworks[]=" + mSpdiNetworks);
- pw.println(" mPnnHomeName=" + mPnnHomeName);
pw.println(" mUsimServiceTable=" + mUsimServiceTable);
pw.println(" mGid1=" + mGid1);
if (mCarrierTestOverride.isInTestMode()) {
diff --git a/com/android/internal/telephony/uicc/UiccCard.java b/com/android/internal/telephony/uicc/UiccCard.java
index 3a71ad3f..2d5e89ea 100644
--- a/com/android/internal/telephony/uicc/UiccCard.java
+++ b/com/android/internal/telephony/uicc/UiccCard.java
@@ -18,8 +18,6 @@ package com.android.internal.telephony.uicc;
import android.app.AlertDialog;
import android.app.usage.UsageStatsManager;
-import android.content.ActivityNotFoundException;
-import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@@ -34,7 +32,6 @@ import android.os.Binder;
import android.os.Handler;
import android.os.Message;
import android.os.PersistableBundle;
-import android.os.PowerManager;
import android.os.Registrant;
import android.os.RegistrantList;
import android.preference.PreferenceManager;
@@ -48,7 +45,6 @@ import android.view.WindowManager;
import com.android.internal.R;
import com.android.internal.telephony.CommandsInterface;
-import com.android.internal.telephony.CommandsInterface.RadioState;
import com.android.internal.telephony.SubscriptionController;
import com.android.internal.telephony.cat.CatService;
import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
@@ -84,14 +80,10 @@ public class UiccCard {
private Context mContext;
private CommandsInterface mCi;
private CatService mCatService;
- private RadioState mLastRadioState = RadioState.RADIO_UNAVAILABLE;
private UiccCarrierPrivilegeRules mCarrierPrivilegeRules;
- private RegistrantList mAbsentRegistrants = new RegistrantList();
private RegistrantList mCarrierPrivilegeRegistrants = new RegistrantList();
- private static final int EVENT_CARD_REMOVED = 13;
- private static final int EVENT_CARD_ADDED = 14;
private static final int EVENT_OPEN_LOGICAL_CHANNEL_DONE = 15;
private static final int EVENT_CLOSE_LOGICAL_CHANNEL_DONE = 16;
private static final int EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE = 17;
@@ -168,24 +160,6 @@ public class UiccCard {
}
sanitizeApplicationIndexesLocked();
-
- RadioState radioState = mCi.getRadioState();
- if (DBG) log("update: radioState=" + radioState + " mLastRadioState="
- + mLastRadioState);
- // No notifications while radio is off or we just powering up
- if (radioState == RadioState.RADIO_ON && mLastRadioState == RadioState.RADIO_ON) {
- if (oldState != CardState.CARDSTATE_ABSENT &&
- mCardState == CardState.CARDSTATE_ABSENT) {
- if (DBG) log("update: notify card removed");
- mAbsentRegistrants.notifyRegistrants();
- mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_REMOVED, null));
- } else if (oldState == CardState.CARDSTATE_ABSENT &&
- mCardState != CardState.CARDSTATE_ABSENT) {
- if (DBG) log("update: notify card added");
- mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_ADDED, null));
- }
- }
- mLastRadioState = radioState;
}
}
@@ -249,27 +223,6 @@ public class UiccCard {
}
/**
- * Notifies handler of any transition into State.ABSENT
- */
- public void registerForAbsent(Handler h, int what, Object obj) {
- synchronized (mLock) {
- Registrant r = new Registrant (h, what, obj);
-
- mAbsentRegistrants.add(r);
-
- if (mCardState == CardState.CARDSTATE_ABSENT) {
- r.notifyRegistrant();
- }
- }
- }
-
- public void unregisterForAbsent(Handler h) {
- synchronized (mLock) {
- mAbsentRegistrants.remove(h);
- }
- }
-
- /**
* Notifies handler when carrier privilege rules are loaded.
*/
public void registerForCarrierPrivilegeRulesLoaded(Handler h, int what, Object obj) {
@@ -290,90 +243,10 @@ public class UiccCard {
}
}
- private void onIccSwap(boolean isAdded) {
-
- boolean isHotSwapSupported = mContext.getResources().getBoolean(
- R.bool.config_hotswapCapable);
-
- if (isHotSwapSupported) {
- log("onIccSwap: isHotSwapSupported is true, don't prompt for rebooting");
- return;
- }
- log("onIccSwap: isHotSwapSupported is false, prompt for rebooting");
-
- promptForRestart(isAdded);
- }
-
- private void promptForRestart(boolean isAdded) {
- synchronized (mLock) {
- final Resources res = mContext.getResources();
- final String dialogComponent = res.getString(
- R.string.config_iccHotswapPromptForRestartDialogComponent);
- if (dialogComponent != null) {
- Intent intent = new Intent().setComponent(ComponentName.unflattenFromString(
- dialogComponent)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(EXTRA_ICC_CARD_ADDED, isAdded);
- try {
- mContext.startActivity(intent);
- return;
- } catch (ActivityNotFoundException e) {
- loge("Unable to find ICC hotswap prompt for restart activity: " + e);
- }
- }
-
- // TODO: Here we assume the device can't handle SIM hot-swap
- // and has to reboot. We may want to add a property,
- // e.g. REBOOT_ON_SIM_SWAP, to indicate if modem support
- // hot-swap.
- DialogInterface.OnClickListener listener = null;
-
-
- // TODO: SimRecords is not reset while SIM ABSENT (only reset while
- // Radio_off_or_not_available). Have to reset in both both
- // added or removed situation.
- listener = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- synchronized (mLock) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- if (DBG) log("Reboot due to SIM swap");
- PowerManager pm = (PowerManager) mContext
- .getSystemService(Context.POWER_SERVICE);
- pm.reboot("SIM is added.");
- }
- }
- }
-
- };
-
- Resources r = Resources.getSystem();
-
- String title = (isAdded) ? r.getString(R.string.sim_added_title) :
- r.getString(R.string.sim_removed_title);
- String message = (isAdded) ? r.getString(R.string.sim_added_message) :
- r.getString(R.string.sim_removed_message);
- String buttonTxt = r.getString(R.string.sim_restart_button);
-
- AlertDialog dialog = new AlertDialog.Builder(mContext)
- .setTitle(title)
- .setMessage(message)
- .setPositiveButton(buttonTxt, listener)
- .create();
- dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
- dialog.show();
- }
- }
-
protected Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg){
switch (msg.what) {
- case EVENT_CARD_REMOVED:
- onIccSwap(false);
- break;
- case EVENT_CARD_ADDED:
- onIccSwap(true);
- break;
case EVENT_OPEN_LOGICAL_CHANNEL_DONE:
case EVENT_CLOSE_LOGICAL_CHANNEL_DONE:
case EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE:
@@ -792,13 +665,7 @@ public class UiccCard {
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("UiccCard:");
pw.println(" mCi=" + mCi);
- pw.println(" mLastRadioState=" + mLastRadioState);
pw.println(" mCatService=" + mCatService);
- pw.println(" mAbsentRegistrants: size=" + mAbsentRegistrants.size());
- for (int i = 0; i < mAbsentRegistrants.size(); i++) {
- pw.println(" mAbsentRegistrants[" + i + "]="
- + ((Registrant)mAbsentRegistrants.get(i)).getHandler());
- }
for (int i = 0; i < mCarrierPrivilegeRegistrants.size(); i++) {
pw.println(" mCarrierPrivilegeRegistrants[" + i + "]="
+ ((Registrant)mCarrierPrivilegeRegistrants.get(i)).getHandler());
diff --git a/com/android/internal/telephony/uicc/UiccController.java b/com/android/internal/telephony/uicc/UiccController.java
index a948b756..c7c802ce 100644
--- a/com/android/internal/telephony/uicc/UiccController.java
+++ b/com/android/internal/telephony/uicc/UiccController.java
@@ -22,15 +22,13 @@ import android.os.Handler;
import android.os.Message;
import android.os.Registrant;
import android.os.RegistrantList;
-import android.os.SystemProperties;
import android.os.storage.StorageManager;
-import android.telephony.TelephonyManager;
import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
import android.text.format.Time;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.PhoneConstants;
-import com.android.internal.telephony.SubscriptionController;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -85,12 +83,21 @@ public class UiccController extends Handler {
public static final int APP_FAM_IMS = 3;
private static final int EVENT_ICC_STATUS_CHANGED = 1;
- private static final int EVENT_GET_ICC_STATUS_DONE = 2;
- private static final int EVENT_RADIO_UNAVAILABLE = 3;
- private static final int EVENT_SIM_REFRESH = 4;
-
+ private static final int EVENT_SLOT_STATUS_CHANGED = 2;
+ private static final int EVENT_ICC_OR_SLOT_STATUS_CHANGED = 3;
+ private static final int EVENT_GET_ICC_STATUS_DONE = 4;
+ private static final int EVENT_RADIO_UNAVAILABLE = 5;
+ private static final int EVENT_SIM_REFRESH = 6;
+
+ // this still needs to be here, because on bootup we dont know which index maps to which
+ // UiccSlot
private CommandsInterface[] mCis;
- private UiccCard[] mUiccCards = new UiccCard[TelephonyManager.getDefault().getPhoneCount()];
+ // todo: add a system property/mk file constant for this
+ private UiccSlot[] mUiccSlots = new UiccSlot[TelephonyManager.getDefault().getPhoneCount()];
+ // flag to indicate if UiccSlots have been initialized. This will be set to true only after the
+ // first slots status is received. That is when phoneId to slotId mapping is known as well.
+ // todo: assuming true for now and hardcoding the mapping between UiccSlot index and phoneId
+ private boolean mUiccSlotsInitialized = true;
private static final Object mLock = new Object();
private static UiccController mInstance;
@@ -120,17 +127,23 @@ public class UiccController extends Handler {
mContext = c;
mCis = ci;
for (int i = 0; i < mCis.length; i++) {
- Integer index = new Integer(i);
+ // todo: get rid of this once hardcoding of mapping between UiccSlot index and phoneId
+ // is removed, instead do this when icc/slot status is received
+ mUiccSlots[i] = new UiccSlot(c, true /* isActive */);
+
+ Integer index = i;
mCis[i].registerForIccStatusChanged(this, EVENT_ICC_STATUS_CHANGED, index);
+ // todo: add registration for slot status changed here
+
// TODO remove this once modem correctly notifies the unsols
// If the device is unencrypted or has been decrypted or FBE is supported,
// i.e. not in cryptkeeper bounce, read SIM when radio state isavailable.
// Else wait for radio to be on. This is needed for the scenario when SIM is locked --
// to avoid overlap of CryptKeeper and SIM unlock screen.
if (!StorageManager.inCryptKeeperBounce()) {
- mCis[i].registerForAvailable(this, EVENT_ICC_STATUS_CHANGED, index);
+ mCis[i].registerForAvailable(this, EVENT_ICC_OR_SLOT_STATUS_CHANGED, index);
} else {
- mCis[i].registerForOn(this, EVENT_ICC_STATUS_CHANGED, index);
+ mCis[i].registerForOn(this, EVENT_ICC_OR_SLOT_STATUS_CHANGED, index);
}
mCis[i].registerForNotAvailable(this, EVENT_RADIO_UNAVAILABLE, index);
mCis[i].registerForIccRefresh(this, EVENT_SIM_REFRESH, index);
@@ -139,6 +152,13 @@ public class UiccController extends Handler {
mLauncher = new UiccStateChangedLauncher(c, this);
}
+ private int getSlotIdFromPhoneId(int phoneId) {
+ // todo: implement (if mUiccSlotsInitialized || if info available about that specific
+ // phoneId which will be the case if sim status for that phoneId is received first)
+ // else return invalid slotId
+ return phoneId;
+ }
+
public static UiccController getInstance() {
synchronized (mLock) {
if (mInstance == null) {
@@ -151,18 +171,74 @@ public class UiccController extends Handler {
public UiccCard getUiccCard(int phoneId) {
synchronized (mLock) {
- if (isValidCardIndex(phoneId)) {
- return mUiccCards[phoneId];
+ return getUiccCardForPhone(phoneId);
+ }
+ }
+
+ /**
+ * API to get UiccCard corresponding to given physical slot index
+ * @param slotId index of physical slot on the device
+ * @return UiccCard object corresponting to given physical slot index; null if card is
+ * absent
+ */
+ public UiccCard getUiccCardForSlot(int slotId) {
+ synchronized (mLock) {
+ UiccSlot uiccSlot = getUiccSlot(slotId);
+ if (uiccSlot != null) {
+ return uiccSlot.getUiccCard();
}
return null;
}
}
- public UiccCard[] getUiccCards() {
+ /**
+ * API to get UiccCard corresponding to given phone id
+ * @return UiccCard object corresponding to given phone id; null if there is no card present for
+ * the phone id
+ */
+ public UiccCard getUiccCardForPhone(int phoneId) {
+ synchronized (mLock) {
+ int slotId = getSlotIdFromPhoneId(phoneId);
+ return getUiccCardForSlot(slotId);
+ }
+ }
+
+ /**
+ * API to get an array of all UiccSlots, which represents all physical slots on the device
+ * @return array of all UiccSlots
+ */
+ public UiccSlot[] getUiccSlots() {
// Return cloned array since we don't want to give out reference
// to internal data structure.
synchronized (mLock) {
- return mUiccCards.clone();
+ return mUiccSlots.clone();
+ }
+ }
+
+ /**
+ * API to get UiccSlot object for a specific physical slot index on the device
+ * @return UiccSlot object for the given physical slot index
+ */
+ public UiccSlot getUiccSlot(int slotId) {
+ synchronized (mLock) {
+ if (isValidSlotIndex(slotId)) {
+ return mUiccSlots[slotId];
+ }
+ return null;
+ }
+ }
+
+ /**
+ * API to get UiccSlot object for a given phone id
+ * @return UiccSlot object for the given phone id
+ */
+ public UiccSlot getUiccSlotForPhone(int phoneId) {
+ synchronized (mLock) {
+ int slotId = getSlotIdFromPhoneId(phoneId);
+ if (isValidSlotIndex(slotId)) {
+ return mUiccSlots[slotId];
+ }
+ return null;
}
}
@@ -209,34 +285,34 @@ public class UiccController extends Handler {
@Override
public void handleMessage (Message msg) {
synchronized (mLock) {
- Integer index = getCiIndex(msg);
+ Integer phoneId = getCiIndex(msg);
- if (index < 0 || index >= mCis.length) {
- Rlog.e(LOG_TAG, "Invalid index : " + index + " received with event " + msg.what);
+ if (phoneId < 0 || phoneId >= mCis.length) {
+ Rlog.e(LOG_TAG, "Invalid phoneId : " + phoneId + " received with event "
+ + msg.what);
return;
}
AsyncResult ar = (AsyncResult)msg.obj;
switch (msg.what) {
case EVENT_ICC_STATUS_CHANGED:
+ case EVENT_ICC_OR_SLOT_STATUS_CHANGED:
if (DBG) log("Received EVENT_ICC_STATUS_CHANGED, calling getIccCardStatus");
- mCis[index].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE, index));
+ mCis[phoneId].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE,
+ phoneId));
break;
case EVENT_GET_ICC_STATUS_DONE:
if (DBG) log("Received EVENT_GET_ICC_STATUS_DONE");
- onGetIccCardStatusDone(ar, index);
+ onGetIccCardStatusDone(ar, phoneId);
break;
case EVENT_RADIO_UNAVAILABLE:
if (DBG) log("EVENT_RADIO_UNAVAILABLE, dispose card");
- if (mUiccCards[index] != null) {
- mUiccCards[index].dispose();
- }
- mUiccCards[index] = null;
- mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, index, null));
+ mUiccSlots[getSlotIdFromPhoneId(phoneId)].onRadioStateUnavailable();
+ mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, phoneId, null));
break;
case EVENT_SIM_REFRESH:
if (DBG) log("Received EVENT_SIM_REFRESH");
- onSimRefresh(ar, index);
+ onSimRefresh(ar, phoneId);
break;
default:
Rlog.e(LOG_TAG, " Unknown Event " + msg.what);
@@ -269,11 +345,9 @@ public class UiccController extends Handler {
// Easy to use API
public UiccCardApplication getUiccCardApplication(int phoneId, int family) {
synchronized (mLock) {
- if (isValidCardIndex(phoneId)) {
- UiccCard c = mUiccCards[phoneId];
- if (c != null) {
- return mUiccCards[phoneId].getApplication(family);
- }
+ UiccCard uiccCard = getUiccCardForPhone(phoneId);
+ if (uiccCard != null) {
+ return uiccCard.getApplication(family);
}
return null;
}
@@ -293,13 +367,7 @@ public class UiccController extends Handler {
IccCardStatus status = (IccCardStatus)ar.result;
- if (mUiccCards[index] == null) {
- //Create new card
- mUiccCards[index] = new UiccCard(mContext, mCis[index], status, index);
- } else {
- //Update already existing card
- mUiccCards[index].update(mContext, mCis[index] , status);
- }
+ mUiccSlots[getSlotIdFromPhoneId(index)].update(mContext, mCis[index], status, index);
if (DBG) log("Notifying IccChangedRegistrants");
mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, index, null));
@@ -320,22 +388,23 @@ public class UiccController extends Handler {
IccRefreshResponse resp = (IccRefreshResponse) ar.result;
Rlog.d(LOG_TAG, "onSimRefresh: " + resp);
- if (mUiccCards[index] == null) {
+ UiccCard uiccCard = getUiccCardForPhone(index);
+ if (uiccCard == null) {
Rlog.e(LOG_TAG,"onSimRefresh: refresh on null card : " + index);
return;
}
if (resp.refreshResult != IccRefreshResponse.REFRESH_RESULT_RESET) {
- Rlog.d(LOG_TAG, "Ignoring non reset refresh: " + resp);
- return;
+ Rlog.d(LOG_TAG, "Ignoring non reset refresh: " + resp);
+ return;
}
Rlog.d(LOG_TAG, "Handling refresh reset: " + resp);
- boolean changed = mUiccCards[index].resetAppWithAid(resp.aid);
+ boolean changed = uiccCard.resetAppWithAid(resp.aid);
if (changed) {
boolean requirePowerOffOnSimRefreshReset = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_requireRadioPowerOffOnSimRefreshReset);
+ com.android.internal.R.bool.config_requireRadioPowerOffOnSimRefreshReset);
if (requirePowerOffOnSimRefreshReset) {
mCis[index].setRadioPower(false, null);
} else {
@@ -346,7 +415,11 @@ public class UiccController extends Handler {
}
private boolean isValidCardIndex(int index) {
- return (index >= 0 && index < mUiccCards.length);
+ return (index >= 0 && index < TelephonyManager.getDefault().getPhoneCount());
+ }
+
+ private boolean isValidSlotIndex(int index) {
+ return (index >= 0 && index < mUiccSlots.length);
}
private void log(String string) {
@@ -374,13 +447,13 @@ public class UiccController extends Handler {
}
pw.println();
pw.flush();
- pw.println(" mUiccCards: size=" + mUiccCards.length);
- for (int i = 0; i < mUiccCards.length; i++) {
- if (mUiccCards[i] == null) {
- pw.println(" mUiccCards[" + i + "]=null");
+ pw.println(" mUiccSlots: size=" + mUiccSlots.length);
+ for (int i = 0; i < mUiccSlots.length; i++) {
+ if (mUiccSlots[i] == null) {
+ pw.println(" mUiccSlots[" + i + "]=null");
} else {
- pw.println(" mUiccCards[" + i + "]=" + mUiccCards[i]);
- mUiccCards[i].dump(fd, pw, args);
+ pw.println(" mUiccSlots[" + i + "]=" + mUiccSlots[i]);
+ mUiccSlots[i].dump(fd, pw, args);
}
}
pw.println("mCardLogs: ");
diff --git a/com/android/internal/telephony/uicc/UiccSlot.java b/com/android/internal/telephony/uicc/UiccSlot.java
new file mode 100644
index 00000000..8102b9b2
--- /dev/null
+++ b/com/android/internal/telephony/uicc/UiccSlot.java
@@ -0,0 +1,312 @@
+/*
+ * 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 com.android.internal.telephony.uicc;
+
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.Registrant;
+import android.os.RegistrantList;
+import android.telephony.Rlog;
+import android.util.LocalLog;
+import android.view.WindowManager;
+
+import com.android.internal.R;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.CommandsInterface.RadioState;
+import com.android.internal.telephony.uicc.IccCardStatus.CardState;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * This class represents a physical slot on the device.
+ */
+public class UiccSlot extends Handler {
+ private static final String TAG = "UiccSlot";
+ private static final boolean DBG = true;
+
+ public static final String EXTRA_ICC_CARD_ADDED =
+ "com.android.internal.telephony.uicc.ICC_CARD_ADDED";
+
+ private final Object mLock = new Object();
+ private boolean mActive;
+ private CardState mCardState;
+ private Context mContext;
+ private CommandsInterface mCi;
+ private UiccCard mUiccCard;
+ private RadioState mLastRadioState = RadioState.RADIO_UNAVAILABLE;
+
+ private RegistrantList mAbsentRegistrants = new RegistrantList();
+
+ private static final int EVENT_CARD_REMOVED = 13;
+ private static final int EVENT_CARD_ADDED = 14;
+
+ private static final LocalLog sLocalLog = new LocalLog(100);
+
+ public UiccSlot(Context c, boolean isActive) {
+ if (DBG) log("Creating");
+ mContext = c;
+ mActive = isActive;
+ mCardState = CardState.CARDSTATE_ABSENT;
+ }
+
+ /**
+ * Update slot. The main trigger for this is a change in the ICC Card status.
+ */
+ public void update(Context c, CommandsInterface ci, IccCardStatus ics, int phoneId) {
+ synchronized (mLock) {
+ CardState oldState = mCardState;
+ mCardState = ics.mCardState;
+ mCi = ci;
+ mContext = c;
+
+ RadioState radioState = mCi.getRadioState();
+ if (DBG) {
+ log("update: radioState=" + radioState + " mLastRadioState=" + mLastRadioState);
+ }
+
+ if (oldState != CardState.CARDSTATE_ABSENT
+ && mCardState == CardState.CARDSTATE_ABSENT) {
+ // No notifications while radio is off or we just powering up
+ if (radioState == RadioState.RADIO_ON && mLastRadioState == RadioState.RADIO_ON) {
+ if (DBG) log("update: notify card removed");
+ mAbsentRegistrants.notifyRegistrants();
+ sendMessage(obtainMessage(EVENT_CARD_REMOVED, null));
+ }
+
+ // no card present in the slot now; dispose card and make mUiccCard null
+ mUiccCard.dispose();
+ mUiccCard = null;
+ } else if (oldState == CardState.CARDSTATE_ABSENT
+ && mCardState != CardState.CARDSTATE_ABSENT) {
+ // No notifications while radio is off or we just powering up
+ if (radioState == RadioState.RADIO_ON && mLastRadioState == RadioState.RADIO_ON) {
+ if (DBG) log("update: notify card added");
+ sendMessage(obtainMessage(EVENT_CARD_ADDED, null));
+ }
+
+ // card is present in the slot now; create new mUiccCard
+ if (mUiccCard != null) {
+ loge("update: mUiccCard != null when card was present; disposing it now");
+ mUiccCard.dispose();
+ }
+
+ mUiccCard = new UiccCard(mContext, mCi, ics, phoneId);
+ } else {
+ if (mUiccCard != null) {
+ mUiccCard.update(mContext, mCi, ics);
+ }
+ }
+ mLastRadioState = radioState;
+ }
+ }
+
+ @Override
+ protected void finalize() {
+ if (DBG) log("UiccSlot finalized");
+ }
+
+ private void onIccSwap(boolean isAdded) {
+
+ boolean isHotSwapSupported = mContext.getResources().getBoolean(
+ R.bool.config_hotswapCapable);
+
+ if (isHotSwapSupported) {
+ log("onIccSwap: isHotSwapSupported is true, don't prompt for rebooting");
+ return;
+ }
+ log("onIccSwap: isHotSwapSupported is false, prompt for rebooting");
+
+ promptForRestart(isAdded);
+ }
+
+ private void promptForRestart(boolean isAdded) {
+ synchronized (mLock) {
+ final Resources res = mContext.getResources();
+ final String dialogComponent = res.getString(
+ R.string.config_iccHotswapPromptForRestartDialogComponent);
+ if (dialogComponent != null) {
+ Intent intent = new Intent().setComponent(ComponentName.unflattenFromString(
+ dialogComponent)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(EXTRA_ICC_CARD_ADDED, isAdded);
+ try {
+ mContext.startActivity(intent);
+ return;
+ } catch (ActivityNotFoundException e) {
+ loge("Unable to find ICC hotswap prompt for restart activity: " + e);
+ }
+ }
+
+ // TODO: Here we assume the device can't handle SIM hot-swap
+ // and has to reboot. We may want to add a property,
+ // e.g. REBOOT_ON_SIM_SWAP, to indicate if modem support
+ // hot-swap.
+ DialogInterface.OnClickListener listener = null;
+
+
+ // TODO: SimRecords is not reset while SIM ABSENT (only reset while
+ // Radio_off_or_not_available). Have to reset in both both
+ // added or removed situation.
+ listener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ synchronized (mLock) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ if (DBG) log("Reboot due to SIM swap");
+ PowerManager pm = (PowerManager) mContext
+ .getSystemService(Context.POWER_SERVICE);
+ pm.reboot("SIM is added.");
+ }
+ }
+ }
+
+ };
+
+ Resources r = Resources.getSystem();
+
+ String title = (isAdded) ? r.getString(R.string.sim_added_title) :
+ r.getString(R.string.sim_removed_title);
+ String message = (isAdded) ? r.getString(R.string.sim_added_message) :
+ r.getString(R.string.sim_removed_message);
+ String buttonTxt = r.getString(R.string.sim_restart_button);
+
+ AlertDialog dialog = new AlertDialog.Builder(mContext)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(buttonTxt, listener)
+ .create();
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ dialog.show();
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_CARD_REMOVED:
+ onIccSwap(false);
+ break;
+ case EVENT_CARD_ADDED:
+ onIccSwap(true);
+ break;
+ default:
+ loge("Unknown Event " + msg.what);
+ }
+ }
+
+ /**
+ * Returns the state of the UiccCard in the slot.
+ * @return
+ */
+ public CardState getCardState() {
+ synchronized (mLock) {
+ return mCardState;
+ }
+ }
+
+ /**
+ * Returns the UiccCard in the slot.
+ */
+ public UiccCard getUiccCard() {
+ synchronized (mLock) {
+ return mUiccCard;
+ }
+ }
+
+ /**
+ * Processes radio state unavailable event
+ */
+ public void onRadioStateUnavailable() {
+ if (mUiccCard != null) {
+ mUiccCard.dispose();
+ }
+ mUiccCard = null;
+ mCardState = CardState.CARDSTATE_ABSENT;
+ mLastRadioState = RadioState.RADIO_UNAVAILABLE;
+ }
+
+ /**
+ * Notifies handler of any transition into State.ABSENT
+ */
+ public void registerForAbsent(Handler h, int what, Object obj) {
+ synchronized (mLock) {
+ Registrant r = new Registrant(h, what, obj);
+
+ mAbsentRegistrants.add(r);
+
+ if (mCardState == CardState.CARDSTATE_ABSENT) {
+ r.notifyRegistrant();
+ }
+ }
+ }
+
+ /**
+ * Unregister a handler for card absent notification
+ */
+ public void unregisterForAbsent(Handler h) {
+ synchronized (mLock) {
+ mAbsentRegistrants.remove(h);
+ }
+ }
+
+ private void log(String msg) {
+ Rlog.d(TAG, msg);
+ }
+
+ private void loge(String msg) {
+ Rlog.e(TAG, msg);
+ }
+
+ private void loglocal(String msg) {
+ if (DBG) sLocalLog.log(msg);
+ }
+
+ /**
+ * Dump
+ */
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("UiccSlot:");
+ pw.println(" mCi=" + mCi);
+ pw.println(" mActive=" + mActive);
+ pw.println(" mLastRadioState=" + mLastRadioState);
+ pw.println(" mAbsentRegistrants: size=" + mAbsentRegistrants.size());
+ for (int i = 0; i < mAbsentRegistrants.size(); i++) {
+ pw.println(" mAbsentRegistrants[" + i + "]="
+ + ((Registrant) mAbsentRegistrants.get(i)).getHandler());
+ }
+ pw.println(" mCardState=" + mCardState);
+ if (mUiccCard != null) {
+ pw.println(" mUiccCard=" + mUiccCard);
+ mUiccCard.dump(fd, pw, args);
+ } else {
+ pw.println(" mUiccCard=null");
+ }
+ pw.println();
+ pw.flush();
+ pw.println("sLocalLog:");
+ sLocalLog.dump(fd, pw, args);
+ pw.flush();
+ }
+}
diff --git a/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java b/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java
index a5ab60ba..a8186419 100644
--- a/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java
+++ b/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java
@@ -65,11 +65,12 @@ public class UiccStateChangedLauncher extends Handler {
mIsRestricted = new boolean[TelephonyManager.getDefault().getPhoneCount()];
shouldNotify = true;
}
- UiccCard[] cards = mUiccController.getUiccCards();
- for (int i = 0; cards != null && i < cards.length; ++i) {
+ UiccSlot[] uiccSlots = mUiccController.getUiccSlots();
+ for (int i = 0; uiccSlots != null && i < uiccSlots.length; ++i) {
// Update only if restricted state changes.
- if ((cards[i] == null
- || cards[i].getCardState() != CardState.CARDSTATE_RESTRICTED)
+ UiccCard uiccCard = uiccSlots[i].getUiccCard();
+ if ((uiccCard == null
+ || uiccCard.getCardState() != CardState.CARDSTATE_RESTRICTED)
!= mIsRestricted[i]) {
mIsRestricted[i] = !mIsRestricted[i];
shouldNotify = true;
diff --git a/com/android/internal/telephony/uicc/asn1/Asn1Decoder.java b/com/android/internal/telephony/uicc/asn1/Asn1Decoder.java
new file mode 100644
index 00000000..1ad0b664
--- /dev/null
+++ b/com/android/internal/telephony/uicc/asn1/Asn1Decoder.java
@@ -0,0 +1,151 @@
+/*
+ * 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 com.android.internal.telephony.uicc.asn1;
+
+import com.android.internal.telephony.uicc.IccUtils;
+
+/**
+ * This represents a decoder helping decode an array of bytes or a hex string into
+ * {@link Asn1Node}s. This class tracks the next position for decoding. This class is not
+ * thread-safe.
+ */
+public final class Asn1Decoder {
+ // Source byte array.
+ private final byte[] mSrc;
+ // Next position of the byte in the source array for decoding.
+ private int mPosition;
+ // Exclusive end of the range in the array for decoding.
+ private final int mEnd;
+
+ /** Creates a decoder on a hex string. */
+ public Asn1Decoder(String hex) {
+ this(IccUtils.hexStringToBytes(hex));
+ }
+
+ /** Creates a decoder on a byte array. */
+ public Asn1Decoder(byte[] src) {
+ this(src, 0, src.length);
+ }
+
+ /**
+ * Creates a decoder on a byte array slice.
+ *
+ * @throws IndexOutOfBoundsException If the range defined by {@code offset} and {@code length}
+ * exceeds the bounds of {@code bytes}.
+ */
+ public Asn1Decoder(byte[] bytes, int offset, int length) {
+ if (offset < 0 || length < 0 || offset + length > bytes.length) {
+ throw new IndexOutOfBoundsException(
+ "Out of the bounds: bytes=["
+ + bytes.length
+ + "], offset="
+ + offset
+ + ", length="
+ + length);
+ }
+ mSrc = bytes;
+ mPosition = offset;
+ mEnd = offset + length;
+ }
+
+ /** @return The next start position for decoding. */
+ public int getPosition() {
+ return mPosition;
+ }
+
+ /** Returns whether the node has a next node. */
+ public boolean hasNextNode() {
+ return mPosition < mEnd;
+ }
+
+ /**
+ * Parses the next node. If the node is a constructed node, its children will be parsed only
+ * when they are accessed, e.g., though {@link Asn1Node#getChildren()}.
+ *
+ * @return The next decoded {@link Asn1Node}. If success, the next decoding position will also
+ * be updated. If any error happens, e.g., moving over the end position, {@code null}
+ * will be returned and the next decoding position won't be modified.
+ * @throws InvalidAsn1DataException If the bytes cannot be parsed.
+ */
+ public Asn1Node nextNode() throws InvalidAsn1DataException {
+ if (mPosition >= mEnd) {
+ throw new IllegalStateException("No bytes to parse.");
+ }
+
+ int offset = mPosition;
+
+ // Extracts the tag.
+ int tagStart = offset;
+ byte b = mSrc[offset++];
+ if ((b & 0x1F) == 0x1F) {
+ // High-tag-number form
+ while (offset < mEnd && (mSrc[offset++] & 0x80) != 0) {
+ // Do nothing.
+ }
+ }
+ if (offset >= mEnd) {
+ // No length bytes or the tag is too long.
+ throw new InvalidAsn1DataException(0, "Invalid length at position: " + offset);
+ }
+ int tag;
+ try {
+ tag = IccUtils.bytesToInt(mSrc, tagStart, offset - tagStart);
+ } catch (IllegalArgumentException e) {
+ // Cannot parse the tag as an integer.
+ throw new InvalidAsn1DataException(0, "Cannot parse tag at position: " + tagStart, e);
+ }
+
+ // Extracts the length.
+ int dataLen;
+ b = mSrc[offset++];
+ if ((b & 0x80) == 0) {
+ // Short-form length
+ dataLen = b;
+ } else {
+ // Long-form length
+ int lenLen = b & 0x7F;
+ if (offset + lenLen > mEnd) {
+ // No enough bytes for the long-form length
+ throw new InvalidAsn1DataException(
+ tag, "Cannot parse length at position: " + offset);
+ }
+ try {
+ dataLen = IccUtils.bytesToInt(mSrc, offset, lenLen);
+ } catch (IllegalArgumentException e) {
+ // Cannot parse the data length as an integer.
+ throw new InvalidAsn1DataException(
+ tag, "Cannot parse length at position: " + offset, e);
+ }
+ offset += lenLen;
+ }
+ if (offset + dataLen > mEnd) {
+ // No enough data left.
+ throw new InvalidAsn1DataException(
+ tag,
+ "Incomplete data at position: "
+ + offset
+ + ", expected bytes: "
+ + dataLen
+ + ", actual bytes: "
+ + (mEnd - offset));
+ }
+
+ Asn1Node root = new Asn1Node(tag, mSrc, offset, dataLen);
+ mPosition = offset + dataLen;
+ return root;
+ }
+}
diff --git a/com/android/internal/telephony/uicc/asn1/Asn1Node.java b/com/android/internal/telephony/uicc/asn1/Asn1Node.java
new file mode 100644
index 00000000..5eb1d5c5
--- /dev/null
+++ b/com/android/internal/telephony/uicc/asn1/Asn1Node.java
@@ -0,0 +1,598 @@
+/*
+ * 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 com.android.internal.telephony.uicc.asn1;
+
+import android.annotation.Nullable;
+
+import com.android.internal.telephony.uicc.IccUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This represents a primitive or constructed data defined by ASN.1. A constructed node can have
+ * child nodes. A non-constructed node can have a value. This class is read-only. To build a node,
+ * you can use the {@link #newBuilder(int)} method to get a {@link Builder} instance. This class is
+ * not thread-safe.
+ */
+public final class Asn1Node {
+ private static final int INT_BYTES = Integer.SIZE / Byte.SIZE;
+ private static final List<Asn1Node> EMPTY_NODE_LIST = Collections.emptyList();
+
+ // Bytes for boolean values.
+ private static final byte[] TRUE_BYTES = new byte[] {-1};
+ private static final byte[] FALSE_BYTES = new byte[] {0};
+
+ /**
+ * This class is used to build an Asn1Node instance of a constructed tag. This class is not
+ * thread-safe.
+ */
+ public static final class Builder {
+ private final int mTag;
+ private final List<Asn1Node> mChildren;
+
+ private Builder(int tag) {
+ if (!isConstructedTag(tag)) {
+ throw new IllegalArgumentException(
+ "Builder should be created for a constructed tag: " + tag);
+ }
+ mTag = tag;
+ mChildren = new ArrayList<>();
+ }
+
+ /**
+ * Adds a child from an existing node.
+ *
+ * @return This builder.
+ * @throws IllegalArgumentException If the child is a non-existing node.
+ */
+ public Builder addChild(Asn1Node child) {
+ mChildren.add(child);
+ return this;
+ }
+
+ /**
+ * Adds a child from another builder. The child will be built with the call to this method,
+ * and any changes to the child builder after the call to this method doesn't have effect.
+ *
+ * @return This builder.
+ */
+ public Builder addChild(Builder child) {
+ mChildren.add(child.build());
+ return this;
+ }
+
+ /**
+ * Adds children from bytes. This method calls {@link Asn1Decoder} to verify the {@code
+ * encodedBytes} and adds all nodes parsed from it as children.
+ *
+ * @return This builder.
+ * @throws InvalidAsn1DataException If the data bytes cannot be parsed.
+ */
+ public Builder addChildren(byte[] encodedBytes) throws InvalidAsn1DataException {
+ Asn1Decoder subDecoder = new Asn1Decoder(encodedBytes, 0, encodedBytes.length);
+ while (subDecoder.hasNextNode()) {
+ mChildren.add(subDecoder.nextNode());
+ }
+ return this;
+ }
+
+ /**
+ * Adds a child of non-constructed tag with an integer as the data.
+ *
+ * @return This builder.
+ * @throws IllegalStateException If the {@code tag} is not constructed..
+ */
+ public Builder addChildAsInteger(int tag, int value) {
+ if (isConstructedTag(tag)) {
+ throw new IllegalStateException("Cannot set value of a constructed tag: " + tag);
+ }
+ byte[] dataBytes = IccUtils.signedIntToBytes(value);
+ addChild(new Asn1Node(tag, dataBytes, 0, dataBytes.length));
+ return this;
+ }
+
+ /**
+ * Adds a child of non-constructed tag with a string as the data.
+ *
+ * @return This builder.
+ * @throws IllegalStateException If the {@code tag} is not constructed..
+ */
+ public Builder addChildAsString(int tag, String value) {
+ if (isConstructedTag(tag)) {
+ throw new IllegalStateException("Cannot set value of a constructed tag: " + tag);
+ }
+ byte[] dataBytes = value.getBytes(StandardCharsets.UTF_8);
+ addChild(new Asn1Node(tag, dataBytes, 0, dataBytes.length));
+ return this;
+ }
+
+ /**
+ * Adds a child of non-constructed tag with a byte array as the data.
+ *
+ * @param value The value will be owned by this node.
+ * @return This builder.
+ * @throws IllegalStateException If the {@code tag} is not constructed..
+ */
+ public Builder addChildAsBytes(int tag, byte[] value) {
+ if (isConstructedTag(tag)) {
+ throw new IllegalStateException("Cannot set value of a constructed tag: " + tag);
+ }
+ addChild(new Asn1Node(tag, value, 0, value.length));
+ return this;
+ }
+
+ /**
+ * Adds a child of non-constructed tag with a byte array as the data from a hex string.
+ *
+ * @return This builder.
+ * @throws IllegalStateException If the {@code tag} is not constructed..
+ */
+ public Builder addChildAsBytesFromHex(int tag, String hex) {
+ return addChildAsBytes(tag, IccUtils.hexStringToBytes(hex));
+ }
+
+ /**
+ * Adds a child of non-constructed tag with bits as the data.
+ *
+ * @return This builder.
+ * @throws IllegalStateException If the {@code tag} is not constructed..
+ */
+ public Builder addChildAsBits(int tag, int value) {
+ if (isConstructedTag(tag)) {
+ throw new IllegalStateException("Cannot set value of a constructed tag: " + tag);
+ }
+ // Always allocate 5 bytes for simplicity.
+ byte[] dataBytes = new byte[INT_BYTES + 1];
+ // Puts the integer into the byte[1-4].
+ value = Integer.reverse(value);
+ int dataLength = 0;
+ for (int i = 1; i < dataBytes.length; i++) {
+ dataBytes[i] = (byte) (value >> ((INT_BYTES - i) * Byte.SIZE));
+ if (dataBytes[i] != 0) {
+ dataLength = i;
+ }
+ }
+ dataLength++;
+ // The first byte is the number of trailing zeros of the last byte.
+ dataBytes[0] = IccUtils.countTrailingZeros(dataBytes[dataLength - 1]);
+ addChild(new Asn1Node(tag, dataBytes, 0, dataLength));
+ return this;
+ }
+
+ /**
+ * Adds a child of non-constructed tag with a boolean as the data.
+ *
+ * @return This builder.
+ * @throws IllegalStateException If the {@code tag} is not constructed..
+ */
+ public Builder addChildAsBoolean(int tag, boolean value) {
+ if (isConstructedTag(tag)) {
+ throw new IllegalStateException("Cannot set value of a constructed tag: " + tag);
+ }
+ addChild(new Asn1Node(tag, value ? TRUE_BYTES : FALSE_BYTES, 0, 1));
+ return this;
+ }
+
+ /** Builds the node. */
+ public Asn1Node build() {
+ return new Asn1Node(mTag, mChildren);
+ }
+ }
+
+ private final int mTag;
+ private final boolean mConstructed;
+ // Do not use this field directly in the methods other than the constructor and encoding
+ // methods (e.g., toBytes()), but always use getChildren() instead.
+ private final List<Asn1Node> mChildren;
+
+ // Byte array that actually holds the data. For a non-constructed node, this stores its actual
+ // value. If the value is not set, this is null. For constructed node, this stores encoded data
+ // of its children, which will be decoded on the first call to getChildren().
+ private @Nullable byte[] mDataBytes;
+ // Offset of the data in above byte array.
+ private int mDataOffset;
+ // Length of the data in above byte array. If it's a constructed node, this is always the total
+ // length of all its children.
+ private int mDataLength;
+ // Length of the total bytes required to encode this node.
+ private int mEncodedLength;
+
+ /**
+ * Creates a new ASN.1 data node builder with the given tag. The tag is an encoded tag including
+ * the tag class, tag number, and constructed mask.
+ */
+ public static Builder newBuilder(int tag) {
+ return new Builder(tag);
+ }
+
+ private static boolean isConstructedTag(int tag) {
+ // Constructed mask is at the 6th bit.
+ byte[] tagBytes = IccUtils.unsignedIntToBytes(tag);
+ return (tagBytes[0] & 0x20) != 0;
+ }
+
+ private static int calculateEncodedBytesNumForLength(int length) {
+ // Constructed mask is at the 6th bit.
+ int len = 1;
+ if (length > 127) {
+ len += IccUtils.byteNumForUnsignedInt(length);
+ }
+ return len;
+ }
+
+ /**
+ * Creates a node with given data bytes. If it is a constructed node, its children will be
+ * parsed when they are visited.
+ */
+ Asn1Node(int tag, @Nullable byte[] src, int offset, int length) {
+ mTag = tag;
+ // Constructed mask is at the 6th bit.
+ mConstructed = isConstructedTag(tag);
+ mDataBytes = src;
+ mDataOffset = offset;
+ mDataLength = length;
+ mChildren = mConstructed ? new ArrayList<Asn1Node>() : EMPTY_NODE_LIST;
+ mEncodedLength =
+ IccUtils.byteNumForUnsignedInt(mTag)
+ + calculateEncodedBytesNumForLength(mDataLength)
+ + mDataLength;
+ }
+
+ /** Creates a constructed node with given children. */
+ private Asn1Node(int tag, List<Asn1Node> children) {
+ mTag = tag;
+ mConstructed = true;
+ mChildren = children;
+
+ mDataLength = 0;
+ int size = children.size();
+ for (int i = 0; i < size; i++) {
+ mDataLength += children.get(i).mEncodedLength;
+ }
+ mEncodedLength =
+ IccUtils.byteNumForUnsignedInt(mTag)
+ + calculateEncodedBytesNumForLength(mDataLength)
+ + mDataLength;
+ }
+
+ public int getTag() {
+ return mTag;
+ }
+
+ public boolean isConstructed() {
+ return mConstructed;
+ }
+
+ /**
+ * Tests if a node has a child.
+ *
+ * @param tag The tag of an immediate child.
+ * @param tags The tags of lineal descendant.
+ */
+ public boolean hasChild(int tag, int... tags) throws InvalidAsn1DataException {
+ try {
+ getChild(tag, tags);
+ } catch (TagNotFoundException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Gets the first child node having the given {@code tag} and {@code tags}.
+ *
+ * @param tag The tag of an immediate child.
+ * @param tags The tags of lineal descendant.
+ * @throws TagNotFoundException If the child cannot be found.
+ */
+ public Asn1Node getChild(int tag, int... tags)
+ throws TagNotFoundException, InvalidAsn1DataException {
+ if (!mConstructed) {
+ throw new TagNotFoundException(tag);
+ }
+ int index = 0;
+ Asn1Node node = this;
+ while (node != null) {
+ List<Asn1Node> children = node.getChildren();
+ int size = children.size();
+ Asn1Node foundChild = null;
+ for (int i = 0; i < size; i++) {
+ Asn1Node child = children.get(i);
+ if (child.getTag() == tag) {
+ foundChild = child;
+ break;
+ }
+ }
+ node = foundChild;
+ if (index >= tags.length) {
+ break;
+ }
+ tag = tags[index++];
+ }
+ if (node == null) {
+ throw new TagNotFoundException(tag);
+ }
+ return node;
+ }
+
+ /**
+ * Gets all child nodes which have the given {@code tag}.
+ *
+ * @return If this is primitive or no such children are found, an empty list will be returned.
+ */
+ public List<Asn1Node> getChildren(int tag)
+ throws TagNotFoundException, InvalidAsn1DataException {
+ if (!mConstructed) {
+ return EMPTY_NODE_LIST;
+ }
+
+ List<Asn1Node> children = getChildren();
+ if (children.isEmpty()) {
+ return EMPTY_NODE_LIST;
+ }
+ List<Asn1Node> output = new ArrayList<>();
+ int size = children.size();
+ for (int i = 0; i < size; i++) {
+ Asn1Node child = children.get(i);
+ if (child.getTag() == tag) {
+ output.add(child);
+ }
+ }
+ return output.isEmpty() ? EMPTY_NODE_LIST : output;
+ }
+
+ /**
+ * Gets all child nodes of this node. If it's a constructed node having encoded data, it's
+ * children will be decoded here.
+ *
+ * @return If this is primitive, an empty list will be returned. Do not modify the returned list
+ * directly.
+ */
+ public List<Asn1Node> getChildren() throws InvalidAsn1DataException {
+ if (!mConstructed) {
+ return EMPTY_NODE_LIST;
+ }
+
+ if (mDataBytes != null) {
+ Asn1Decoder subDecoder = new Asn1Decoder(mDataBytes, mDataOffset, mDataLength);
+ while (subDecoder.hasNextNode()) {
+ mChildren.add(subDecoder.nextNode());
+ }
+ mDataBytes = null;
+ mDataOffset = 0;
+ }
+ return mChildren;
+ }
+
+ /** @return Whether this node has a value. False will be returned for a constructed node. */
+ public boolean hasValue() {
+ return !mConstructed && mDataBytes != null;
+ }
+
+ /**
+ * @return The data as an integer. If the data length is larger than 4, only the first 4 bytes
+ * will be parsed.
+ * @throws InvalidAsn1DataException If the data bytes cannot be parsed.
+ */
+ public int asInteger() throws InvalidAsn1DataException {
+ if (mConstructed) {
+ throw new IllegalStateException("Cannot get value of a constructed node.");
+ }
+ if (mDataBytes == null) {
+ throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null.");
+ }
+ try {
+ return IccUtils.bytesToInt(mDataBytes, mDataOffset, mDataLength);
+ } catch (IllegalArgumentException | IndexOutOfBoundsException e) {
+ throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e);
+ }
+ }
+
+ /**
+ * @return The data as a long variable which can be both positive and negative. If the data
+ * length is larger than 8, only the first 8 bytes will be parsed.
+ * @throws InvalidAsn1DataException If the data bytes cannot be parsed.
+ */
+ public long asRawLong() throws InvalidAsn1DataException {
+ if (mConstructed) {
+ throw new IllegalStateException("Cannot get value of a constructed node.");
+ }
+ if (mDataBytes == null) {
+ throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null.");
+ }
+ try {
+ return IccUtils.bytesToRawLong(mDataBytes, mDataOffset, mDataLength);
+ } catch (IllegalArgumentException | IndexOutOfBoundsException e) {
+ throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e);
+ }
+ }
+
+ /**
+ * @return The data as a string in UTF-8 encoding.
+ * @throws InvalidAsn1DataException If the data bytes cannot be parsed.
+ */
+ public String asString() throws InvalidAsn1DataException {
+ if (mConstructed) {
+ throw new IllegalStateException("Cannot get value of a constructed node.");
+ }
+ if (mDataBytes == null) {
+ throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null.");
+ }
+ try {
+ return new String(mDataBytes, mDataOffset, mDataLength, StandardCharsets.UTF_8);
+ } catch (IndexOutOfBoundsException e) {
+ throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e);
+ }
+ }
+
+ /**
+ * @return The data as a byte array.
+ * @throws InvalidAsn1DataException If the data bytes cannot be parsed.
+ */
+ public byte[] asBytes() throws InvalidAsn1DataException {
+ if (mConstructed) {
+ throw new IllegalStateException("Cannot get value of a constructed node.");
+ }
+ if (mDataBytes == null) {
+ throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null.");
+ }
+ byte[] output = new byte[mDataLength];
+ try {
+ System.arraycopy(mDataBytes, mDataOffset, output, 0, mDataLength);
+ } catch (IndexOutOfBoundsException e) {
+ throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e);
+ }
+ return output;
+ }
+
+ /**
+ * Gets the data as an integer for BIT STRING. DER actually stores the bits in a reversed order.
+ * The returned integer here has the order fixed (first bit is at the lowest position). This
+ * method currently only support at most 32 bits which fit in an integer.
+ *
+ * @return The data as an integer. If this is constructed, a {@code null} will be returned.
+ * @throws InvalidAsn1DataException If the data bytes cannot be parsed.
+ */
+ public int asBits() throws InvalidAsn1DataException {
+ if (mConstructed) {
+ throw new IllegalStateException("Cannot get value of a constructed node.");
+ }
+ if (mDataBytes == null) {
+ throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null.");
+ }
+ int bits;
+ try {
+ bits = IccUtils.bytesToInt(mDataBytes, mDataOffset + 1, mDataLength - 1);
+ } catch (IllegalArgumentException | IndexOutOfBoundsException e) {
+ throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e);
+ }
+ for (int i = mDataLength - 1; i < INT_BYTES; i++) {
+ bits <<= Byte.SIZE;
+ }
+ return Integer.reverse(bits);
+ }
+
+ /**
+ * @return The data as a boolean.
+ * @throws InvalidAsn1DataException If the data bytes cannot be parsed.
+ */
+ public boolean asBoolean() throws InvalidAsn1DataException {
+ if (mConstructed) {
+ throw new IllegalStateException("Cannot get value of a constructed node.");
+ }
+ if (mDataBytes == null) {
+ throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null.");
+ }
+ if (mDataLength != 1) {
+ throw new InvalidAsn1DataException(
+ mTag, "Cannot parse data bytes as boolean: length=" + mDataLength);
+ }
+ if (mDataOffset < 0 || mDataOffset >= mDataBytes.length) {
+ throw new InvalidAsn1DataException(
+ mTag,
+ "Cannot parse data bytes.",
+ new ArrayIndexOutOfBoundsException(mDataOffset));
+ }
+ // ASN.1 has "true" as 0xFF.
+ if (mDataBytes[mDataOffset] == -1) {
+ return Boolean.TRUE;
+ } else if (mDataBytes[mDataOffset] == 0) {
+ return Boolean.FALSE;
+ }
+ throw new InvalidAsn1DataException(
+ mTag, "Cannot parse data bytes as boolean: " + mDataBytes[mDataOffset]);
+ }
+
+ /** @return The number of required bytes for encoding this node in DER. */
+ public int getEncodedLength() {
+ return mEncodedLength;
+ }
+
+ /** @return The number of required bytes for encoding this node's data in DER. */
+ public int getDataLength() {
+ return mDataLength;
+ }
+
+ /**
+ * Writes the DER encoded bytes of this node into a byte array. The number of written bytes is
+ * {@link #getEncodedLength()}.
+ *
+ * @throws IndexOutOfBoundsException If the {@code dest} doesn't have enough space to write.
+ */
+ public void writeToBytes(byte[] dest, int offset) {
+ if (offset < 0 || offset + mEncodedLength > dest.length) {
+ throw new IndexOutOfBoundsException(
+ "Not enough space to write. Required bytes: " + mEncodedLength);
+ }
+ write(dest, offset);
+ }
+
+ /** Writes the DER encoded bytes of this node into a new byte array. */
+ public byte[] toBytes() {
+ byte[] dest = new byte[mEncodedLength];
+ write(dest, 0);
+ return dest;
+ }
+
+ /** Gets a hex string representing the DER encoded bytes of this node. */
+ public String toHex() {
+ return IccUtils.bytesToHexString(toBytes());
+ }
+
+ /** Gets header (tag + length) as hex string. */
+ public String getHeadAsHex() {
+ String headHex = IccUtils.bytesToHexString(IccUtils.unsignedIntToBytes(mTag));
+ if (mDataLength <= 127) {
+ headHex += IccUtils.byteToHex((byte) mDataLength);
+ } else {
+ byte[] lenBytes = IccUtils.unsignedIntToBytes(mDataLength);
+ headHex += IccUtils.byteToHex((byte) (lenBytes.length | 0x80));
+ headHex += IccUtils.bytesToHexString(lenBytes);
+ }
+ return headHex;
+ }
+
+ /** Returns the new offset where to write the next node data. */
+ private int write(byte[] dest, int offset) {
+ // Writes the tag.
+ offset += IccUtils.unsignedIntToBytes(mTag, dest, offset);
+ // Writes the length.
+ if (mDataLength <= 127) {
+ dest[offset++] = (byte) mDataLength;
+ } else {
+ // Bytes required for encoding the length
+ int lenLen = IccUtils.unsignedIntToBytes(mDataLength, dest, ++offset);
+ dest[offset - 1] = (byte) (lenLen | 0x80);
+ offset += lenLen;
+ }
+ // Writes the data.
+ if (mConstructed && mDataBytes == null) {
+ int size = mChildren.size();
+ for (int i = 0; i < size; i++) {
+ Asn1Node child = mChildren.get(i);
+ offset = child.write(dest, offset);
+ }
+ } else if (mDataBytes != null) {
+ System.arraycopy(mDataBytes, mDataOffset, dest, offset, mDataLength);
+ offset += mDataLength;
+ }
+ return offset;
+ }
+}
diff --git a/com/android/internal/telephony/uicc/asn1/InvalidAsn1DataException.java b/com/android/internal/telephony/uicc/asn1/InvalidAsn1DataException.java
new file mode 100644
index 00000000..c151468b
--- /dev/null
+++ b/com/android/internal/telephony/uicc/asn1/InvalidAsn1DataException.java
@@ -0,0 +1,45 @@
+/*
+ * 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 com.android.internal.telephony.uicc.asn1;
+
+/**
+ * Exception for invalid ASN.1 data in DER encoding which cannot be parsed as a node or a specific
+ * data type.
+ */
+public class InvalidAsn1DataException extends Exception {
+ private final int mTag;
+
+ public InvalidAsn1DataException(int tag, String message) {
+ super(message);
+ mTag = tag;
+ }
+
+ public InvalidAsn1DataException(int tag, String message, Throwable throwable) {
+ super(message, throwable);
+ mTag = tag;
+ }
+
+ /** @return The tag which has the invalid data. */
+ public int getTag() {
+ return mTag;
+ }
+
+ @Override
+ public String getMessage() {
+ return super.getMessage() + " (tag=" + mTag + ")";
+ }
+}
diff --git a/com/android/internal/telephony/uicc/asn1/TagNotFoundException.java b/com/android/internal/telephony/uicc/asn1/TagNotFoundException.java
new file mode 100644
index 00000000..f79021eb
--- /dev/null
+++ b/com/android/internal/telephony/uicc/asn1/TagNotFoundException.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.android.internal.telephony.uicc.asn1;
+
+/**
+ * Exception for getting a child of a {@link Asn1Node} with a non-existing tag.
+ */
+public class TagNotFoundException extends Exception {
+ private final int mTag;
+
+ public TagNotFoundException(int tag) {
+ mTag = tag;
+ }
+
+ /** @return The tag which has the invalid data. */
+ public int getTag() {
+ return mTag;
+ }
+
+ @Override
+ public String getMessage() {
+ return super.getMessage() + " (tag=" + mTag + ")";
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/async/AsyncResultCallback.java b/com/android/internal/telephony/uicc/euicc/async/AsyncResultCallback.java
new file mode 100644
index 00000000..6e936871
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/async/AsyncResultCallback.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 com.android.internal.telephony.uicc.euicc.async;
+
+/**
+ * Class to deliver the returned value from an asynchronous call. Either {@link #onResult(Result)}
+ * or {@link #onException(Throwable)} will be called. You can create an anonymous subclass and
+ * override these methods to handle the result or the throwable from an asynchronous call, for
+ * example:
+ *
+ * <pre>
+ * doSomethingAsync(
+ * new AsyncResultCallback&lt;Result&gt;() {
+ * void onResult(Result r) {
+ * Log.i("Got the result: %s", r.toString());
+ * }
+ *
+ * void onException(Throwable e) {...}
+ * });
+ * <pre>
+ *
+ * @param <Result> The returned value of the asynchronous call.
+ * @hide
+ */
+public abstract class AsyncResultCallback<Result> {
+
+ /** This will be called when the result is returned. */
+ public abstract void onResult(Result result);
+
+ /** This will be called when any exception is thrown. */
+ public void onException(Throwable e) {}
+}
diff --git a/com/android/internal/telephony/uicc/euicc/async/AsyncResultHelper.java b/com/android/internal/telephony/uicc/euicc/async/AsyncResultHelper.java
new file mode 100644
index 00000000..d677ed8a
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/async/AsyncResultHelper.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 com.android.internal.telephony.uicc.euicc.async;
+
+import android.annotation.Nullable;
+import android.os.Handler;
+
+/**
+ * Helper on {@link AsyncResultCallback}.
+ *
+ * @hide
+ */
+public final class AsyncResultHelper {
+ /**
+ * Calls the {@code callback} to return the {@code result} object. The {@code callback} will be
+ * run in the {@code handler}. If the {@code handler} is null, the callback will be called
+ * immediately.
+ *
+ * @param <T> Result type.
+ */
+ public static <T> void returnResult(
+ final T result, final AsyncResultCallback<T> callback, @Nullable Handler handler) {
+ if (handler == null) {
+ callback.onResult(result);
+ } else {
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ callback.onResult(result);
+ }
+ });
+ }
+ }
+
+ /**
+ * Calls the {@code callback} to return the thrown {@code e} exception. The {@code callback}
+ * will be run in the {@code handler}. If the {@code handler} is null, the callback will be
+ * called immediately.
+ */
+ public static void throwException(
+ final Throwable e, final AsyncResultCallback<?> callback, @Nullable Handler handler) {
+ if (handler == null) {
+ callback.onException(e);
+ } else {
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ callback.onException(e);
+ }
+ });
+ }
+ }
+
+ private AsyncResultHelper() {}
+}
diff --git a/com/android/internal/util/ArrayUtils.java b/com/android/internal/util/ArrayUtils.java
index 22bfcc34..aa856688 100644
--- a/com/android/internal/util/ArrayUtils.java
+++ b/com/android/internal/util/ArrayUtils.java
@@ -292,6 +292,15 @@ public class ArrayUtils {
return array;
}
+ public static @Nullable long[] convertToLongArray(@Nullable int[] intArray) {
+ if (intArray == null) return null;
+ long[] array = new long[intArray.length];
+ for (int i = 0; i < intArray.length; i++) {
+ array[i] = (long) intArray[i];
+ }
+ return array;
+ }
+
/**
* Adds value to given array if not already present, providing set-like
* behavior.
@@ -425,14 +434,17 @@ public class ArrayUtils {
* Adds value to given array if not already present, providing set-like
* behavior.
*/
- public static @NonNull long[] appendLong(@Nullable long[] cur, long val) {
+ public static @NonNull long[] appendLong(@Nullable long[] cur, long val,
+ boolean allowDuplicates) {
if (cur == null) {
return new long[] { val };
}
final int N = cur.length;
- for (int i = 0; i < N; i++) {
- if (cur[i] == val) {
- return cur;
+ if (!allowDuplicates) {
+ for (int i = 0; i < N; i++) {
+ if (cur[i] == val) {
+ return cur;
+ }
}
}
long[] ret = new long[N + 1];
@@ -442,6 +454,14 @@ public class ArrayUtils {
}
/**
+ * Adds value to given array if not already present, providing set-like
+ * behavior.
+ */
+ public static @NonNull long[] appendLong(@Nullable long[] cur, long val) {
+ return appendLong(cur, val, false);
+ }
+
+ /**
* Removes value from given array if present, providing set-like behavior.
*/
public static @Nullable long[] removeLong(@Nullable long[] cur, long val) {
diff --git a/com/android/internal/util/CollectionUtils.java b/com/android/internal/util/CollectionUtils.java
index f0b47de8..7985e574 100644
--- a/com/android/internal/util/CollectionUtils.java
+++ b/com/android/internal/util/CollectionUtils.java
@@ -30,7 +30,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
-import java.util.function.*;
+import java.util.function.Function;
import java.util.stream.Stream;
/**
@@ -290,11 +290,11 @@ public class CollectionUtils {
if (cur instanceof ArraySet) {
ArraySet<T> arraySet = (ArraySet<T>) cur;
for (int i = 0; i < size; i++) {
- action.accept(arraySet.valueAt(i));
+ action.acceptOrThrow(arraySet.valueAt(i));
}
} else {
for (T t : cur) {
- action.accept(t);
+ action.acceptOrThrow(t);
}
}
} catch (Exception e) {
diff --git a/com/android/internal/util/FunctionalUtils.java b/com/android/internal/util/FunctionalUtils.java
index cdef97e8..82ac2412 100644
--- a/com/android/internal/util/FunctionalUtils.java
+++ b/com/android/internal/util/FunctionalUtils.java
@@ -16,6 +16,9 @@
package com.android.internal.util;
+import android.os.RemoteException;
+
+import java.util.function.Consumer;
import java.util.function.Supplier;
/**
@@ -25,6 +28,21 @@ public class FunctionalUtils {
private FunctionalUtils() {}
/**
+ * Converts a lambda expression that throws a checked exception(s) into a regular
+ * {@link Consumer} by propagating any checked exceptions as {@link RuntimeException}
+ */
+ public static <T> Consumer<T> uncheckExceptions(ThrowingConsumer<T> action) {
+ return action;
+ }
+
+ /**
+ *
+ */
+ public static <T> Consumer<T> ignoreRemoteException(RemoteExceptionIgnoringConsumer<T> action) {
+ return action;
+ }
+
+ /**
* An equivalent of {@link Runnable} that allows throwing checked exceptions
*
* This can be used to specify a lambda argument without forcing all the checked exceptions
@@ -32,7 +50,7 @@ public class FunctionalUtils {
*/
@FunctionalInterface
public interface ThrowingRunnable {
- void run() throws Exception;
+ void runOrThrow() throws Exception;
}
/**
@@ -43,17 +61,47 @@ public class FunctionalUtils {
*/
@FunctionalInterface
public interface ThrowingSupplier<T> {
- T get() throws Exception;
+ T getOrThrow() throws Exception;
}
/**
- * An equivalent of {@link java.util.function.Consumer} that allows throwing checked exceptions
+ * A {@link Consumer} that allows throwing checked exceptions from its single abstract method.
*
- * This can be used to specify a lambda argument without forcing all the checked exceptions
- * to be handled within it
+ * Can be used together with {@link #uncheckExceptions} to effectively turn a lambda expression
+ * that throws a checked exception into a regular {@link Consumer}
+ */
+ @FunctionalInterface
+ @SuppressWarnings("FunctionalInterfaceMethodChanged")
+ public interface ThrowingConsumer<T> extends Consumer<T> {
+ void acceptOrThrow(T t) throws Exception;
+
+ @Override
+ default void accept(T t) {
+ try {
+ acceptOrThrow(t);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ }
+
+ /**
+ * A {@link Consumer} that automatically ignores any {@link RemoteException}s.
+ *
+ * Used by {@link #ignoreRemoteException}
*/
@FunctionalInterface
- public interface ThrowingConsumer<T> {
- void accept(T t) throws Exception;
+ @SuppressWarnings("FunctionalInterfaceMethodChanged")
+ public interface RemoteExceptionIgnoringConsumer<T> extends Consumer<T> {
+ void acceptOrThrow(T t) throws RemoteException;
+
+ @Override
+ default void accept(T t) {
+ try {
+ acceptOrThrow(t);
+ } catch (RemoteException ex) {
+ // ignore
+ }
+ }
}
}
diff --git a/com/android/keyguard/LatencyTracker.java b/com/android/internal/util/LatencyTracker.java
index cee0afcd..72cd2488 100644
--- a/com/android/keyguard/LatencyTracker.java
+++ b/com/android/internal/util/LatencyTracker.java
@@ -1,20 +1,18 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
*/
-package com.android.keyguard;
+package com.android.internal.util;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -28,7 +26,7 @@ import android.util.EventLog;
import android.util.Log;
import android.util.SparseLongArray;
-import com.android.systemui.EventLogTags;
+import com.android.internal.logging.EventLogTags;
/**
* Class to track various latencies in SystemUI. It then outputs the latency to logcat so these
@@ -76,13 +74,19 @@ public class LatencyTracker {
*/
public static final int ACTION_TURN_ON_SCREEN = 5;
+ /**
+ * Time it takes to rotate the screen.
+ */
+ public static final int ACTION_ROTATE_SCREEN = 6;
+
private static final String[] NAMES = new String[] {
"expand panel",
"toggle recents",
"fingerprint wake-and-unlock",
"check credential",
"check credential unlocked",
- "turn on screen" };
+ "turn on screen",
+ "rotate the screen"};
private static LatencyTracker sLatencyTracker;
diff --git a/com/android/internal/util/Preconditions.java b/com/android/internal/util/Preconditions.java
index e5d57167..91c76afd 100644
--- a/com/android/internal/util/Preconditions.java
+++ b/com/android/internal/util/Preconditions.java
@@ -494,4 +494,38 @@ public class Preconditions {
return value;
}
+
+ /**
+ * Ensures that all elements in the argument integer array are within the inclusive range
+ *
+ * @param value an integer array of values
+ * @param lower the lower endpoint of the inclusive range
+ * @param upper the upper endpoint of the inclusive range
+ * @param valueName the name of the argument to use if the check fails
+ *
+ * @return the validated integer array
+ *
+ * @throws IllegalArgumentException if any of the elements in {@code value} were out of range
+ * @throws NullPointerException if the {@code value} was {@code null}
+ */
+ public static int[] checkArrayElementsInRange(int[] value, int lower, int upper,
+ String valueName) {
+ checkNotNull(value, valueName + " must not be null");
+
+ for (int i = 0; i < value.length; ++i) {
+ int v = value[i];
+
+ if (v < lower) {
+ throw new IllegalArgumentException(
+ String.format("%s[%d] is out of range of [%d, %d] (too low)",
+ valueName, i, lower, upper));
+ } else if (v > upper) {
+ throw new IllegalArgumentException(
+ String.format("%s[%d] is out of range of [%d, %d] (too high)",
+ valueName, i, lower, upper));
+ }
+ }
+
+ return value;
+ }
}
diff --git a/com/android/internal/util/function/QuadConsumer.java b/com/android/internal/util/function/QuadConsumer.java
new file mode 100644
index 00000000..d899c01b
--- /dev/null
+++ b/com/android/internal/util/function/QuadConsumer.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.android.internal.util.function;
+
+
+import java.util.function.Consumer;
+
+/**
+ * A 4-argument {@link Consumer}
+ *
+ * @hide
+ */
+public interface QuadConsumer<A, B, C, D> {
+ void accept(A a, B b, C c, D d);
+}
diff --git a/com/android/internal/util/function/QuadFunction.java b/com/android/internal/util/function/QuadFunction.java
new file mode 100644
index 00000000..700d9536
--- /dev/null
+++ b/com/android/internal/util/function/QuadFunction.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.android.internal.util.function;
+
+
+import java.util.function.Function;
+
+/**
+ * A 4-argument {@link Function}
+ *
+ * @hide
+ */
+public interface QuadFunction<A, B, C, D, R> {
+ R apply(A a, B b, C c, D d);
+}
diff --git a/com/android/internal/util/function/QuadPredicate.java b/com/android/internal/util/function/QuadPredicate.java
new file mode 100644
index 00000000..512c98ba
--- /dev/null
+++ b/com/android/internal/util/function/QuadPredicate.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.android.internal.util.function;
+
+
+import java.util.function.Predicate;
+
+/**
+ * A 4-argument {@link Predicate}
+ *
+ * @hide
+ */
+public interface QuadPredicate<A, B, C, D> {
+ boolean test(A a, B b, C c, D d);
+}
diff --git a/com/android/internal/util/function/TriConsumer.java b/com/android/internal/util/function/TriConsumer.java
new file mode 100644
index 00000000..40d614ec
--- /dev/null
+++ b/com/android/internal/util/function/TriConsumer.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.android.internal.util.function;
+
+
+import java.util.function.Consumer;
+
+/**
+ * A 3-argument {@link Consumer}
+ *
+ * @hide
+ */
+public interface TriConsumer<A, B, C> {
+ void accept(A a, B b, C c);
+}
diff --git a/com/android/server/timezone/ClockHelper.java b/com/android/internal/util/function/TriFunction.java
index 353728a1..2b1df86e 100644
--- a/com/android/server/timezone/ClockHelper.java
+++ b/com/android/internal/util/function/TriFunction.java
@@ -14,12 +14,16 @@
* limitations under the License.
*/
-package com.android.server.timezone;
+package com.android.internal.util.function;
+
+
+import java.util.function.Function;
/**
- * An easy-to-mock interface for obtaining a monotonically increasing time value in milliseconds.
+ * A 3-argument {@link Function}
+ *
+ * @hide
*/
-interface ClockHelper {
-
- long currentTimestamp();
+public interface TriFunction<A, B, C, R> {
+ R apply(A a, B b, C c);
}
diff --git a/com/android/internal/util/function/TriPredicate.java b/com/android/internal/util/function/TriPredicate.java
new file mode 100644
index 00000000..d9cd9683
--- /dev/null
+++ b/com/android/internal/util/function/TriPredicate.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.android.internal.util.function;
+
+
+import java.util.function.Predicate;
+
+/**
+ * A 3-argument {@link Predicate}
+ *
+ * @hide
+ */
+public interface TriPredicate<A, B, C> {
+ boolean test(A a, B b, C c);
+}
diff --git a/com/android/internal/util/function/pooled/ArgumentPlaceholder.java b/com/android/internal/util/function/pooled/ArgumentPlaceholder.java
new file mode 100644
index 00000000..cf86b717
--- /dev/null
+++ b/com/android/internal/util/function/pooled/ArgumentPlaceholder.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.android.internal.util.function.pooled;
+
+/**
+ * A placeholder for an argument of type {@code R}
+ *
+ * @see PooledLambda
+ * @hide
+ */
+public final class ArgumentPlaceholder<R> {
+ private ArgumentPlaceholder() {}
+ static final ArgumentPlaceholder<?> INSTANCE = new ArgumentPlaceholder<>();
+
+ @Override
+ public String toString() {
+ return "_";
+ }
+}
diff --git a/com/android/internal/util/function/pooled/OmniFunction.java b/com/android/internal/util/function/pooled/OmniFunction.java
new file mode 100644
index 00000000..c0f506ec
--- /dev/null
+++ b/com/android/internal/util/function/pooled/OmniFunction.java
@@ -0,0 +1,132 @@
+/*
+ * 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 com.android.internal.util.function.pooled;
+
+import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
+import com.android.internal.util.FunctionalUtils.ThrowingSupplier;
+import com.android.internal.util.function.QuadConsumer;
+import com.android.internal.util.function.QuadFunction;
+import com.android.internal.util.function.TriConsumer;
+import com.android.internal.util.function.TriFunction;
+
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+/**
+ * An interface implementing all supported function interfaces, delegating each to {@link #invoke}
+ *
+ * @hide
+ */
+abstract class OmniFunction<A, B, C, D, R> implements
+ PooledFunction<A, R>, BiFunction<A, B, R>, TriFunction<A, B, C, R>,
+ QuadFunction<A, B, C, D, R>,
+ PooledConsumer<A>, BiConsumer<A, B>, TriConsumer<A, B, C>, QuadConsumer<A, B, C, D>,
+ PooledPredicate<A>, BiPredicate<A, B>,
+ PooledSupplier<R>, PooledRunnable,
+ ThrowingRunnable, ThrowingSupplier<R>,
+ PooledSupplier.OfInt, PooledSupplier.OfLong, PooledSupplier.OfDouble {
+
+ abstract R invoke(A a, B b, C c, D d);
+
+ @Override
+ public R apply(A o, B o2) {
+ return invoke(o, o2, null, null);
+ }
+
+ @Override
+ public R apply(A o) {
+ return invoke(o, null, null, null);
+ }
+
+ abstract public <V> OmniFunction<A, B, C, D, V> andThen(Function<? super R, ? extends V> after);
+ abstract public OmniFunction<A, B, C, D, R> negate();
+
+ @Override
+ public void accept(A o, B o2) {
+ invoke(o, o2, null, null);
+ }
+
+ @Override
+ public void accept(A o) {
+ invoke(o, null, null, null);
+ }
+
+ @Override
+ public void run() {
+ invoke(null, null, null, null);
+ }
+
+ @Override
+ public R get() {
+ return invoke(null, null, null, null);
+ }
+
+ @Override
+ public boolean test(A o, B o2) {
+ return (Boolean) invoke(o, o2, null, null);
+ }
+
+ @Override
+ public boolean test(A o) {
+ return (Boolean) invoke(o, null, null, null);
+ }
+
+ @Override
+ public PooledRunnable asRunnable() {
+ return this;
+ }
+
+ @Override
+ public PooledConsumer<A> asConsumer() {
+ return this;
+ }
+
+ @Override
+ public R apply(A a, B b, C c) {
+ return invoke(a, b, c, null);
+ }
+
+ @Override
+ public void accept(A a, B b, C c) {
+ invoke(a, b, c, null);
+ }
+
+ @Override
+ public R apply(A a, B b, C c, D d) {
+ return invoke(a, b, c, d);
+ }
+
+ @Override
+ public void accept(A a, B b, C c, D d) {
+ invoke(a, b, c, d);
+ }
+
+ @Override
+ public void runOrThrow() throws Exception {
+ run();
+ }
+
+ @Override
+ public R getOrThrow() throws Exception {
+ return get();
+ }
+
+ @Override
+ abstract public OmniFunction<A, B, C, D, R> recycleOnUse();
+}
diff --git a/com/android/internal/util/function/pooled/PooledConsumer.java b/com/android/internal/util/function/pooled/PooledConsumer.java
new file mode 100644
index 00000000..f66586ee
--- /dev/null
+++ b/com/android/internal/util/function/pooled/PooledConsumer.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.android.internal.util.function.pooled;
+
+import java.util.function.Consumer;
+
+/**
+ * {@link Consumer} + {@link PooledLambda}
+ *
+ * @see PooledLambda
+ * @hide
+ */
+public interface PooledConsumer<T> extends PooledLambda, Consumer<T> {
+
+ /** @inheritDoc */
+ PooledConsumer<T> recycleOnUse();
+}
diff --git a/com/android/internal/util/function/pooled/PooledFunction.java b/com/android/internal/util/function/pooled/PooledFunction.java
new file mode 100644
index 00000000..1f166faf
--- /dev/null
+++ b/com/android/internal/util/function/pooled/PooledFunction.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.android.internal.util.function.pooled;
+
+import java.util.function.Function;
+
+/**
+ * {@link Function} + {@link PooledLambda}
+ *
+ * @see PooledLambda
+ * @hide
+ */
+public interface PooledFunction<A, R> extends PooledLambda, Function<A, R> {
+
+ /**
+ * Ignores the result
+ */
+ PooledConsumer<A> asConsumer();
+
+ /** @inheritDoc */
+ PooledFunction<A, R> recycleOnUse();
+}
diff --git a/com/android/internal/util/function/pooled/PooledLambda.java b/com/android/internal/util/function/pooled/PooledLambda.java
new file mode 100644
index 00000000..87c25e96
--- /dev/null
+++ b/com/android/internal/util/function/pooled/PooledLambda.java
@@ -0,0 +1,828 @@
+/*
+ * 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 com.android.internal.util.function.pooled;
+
+import static com.android.internal.util.function.pooled.PooledLambdaImpl.acquire;
+import static com.android.internal.util.function.pooled.PooledLambdaImpl.acquireConstSupplier;
+
+import android.os.Message;
+
+import com.android.internal.util.function.QuadConsumer;
+import com.android.internal.util.function.QuadFunction;
+import com.android.internal.util.function.TriConsumer;
+import com.android.internal.util.function.TriFunction;
+import com.android.internal.util.function.pooled.PooledLambdaImpl.LambdaType.ReturnType;
+
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * A recyclable anonymous function.
+ * Allows obtaining {@link Function}s/{@link Runnable}s/{@link Supplier}s/etc. without allocating a
+ * new instance each time
+ *
+ * This exploits the mechanic that stateless lambdas (such as plain/non-bound method references)
+ * get translated into a singleton instance, making it possible to create a recyclable container
+ * ({@link PooledLambdaImpl}) holding a reference to such a singleton function, as well as
+ * (possibly partial) arguments required for its invocation.
+ *
+ * To obtain an instance, use one of the factory methods in this class.
+ *
+ * You can call {@link #recycleOnUse} to make the instance automatically recycled upon invocation,
+ * making if effectively <b>one-time use</b>.
+ * This is often the behavior you want, as it allows to not worry about manual recycling.
+ * Some notable examples: {@link android.os.Handler#post(Runnable)},
+ * {@link android.app.Activity#runOnUiThread(Runnable)}, {@link android.view.View#post(Runnable)}
+ *
+ * For factories of functions that take further arguments, the corresponding 'missing' argument's
+ * position is marked by an argument of type {@link ArgumentPlaceholder} with the type parameter
+ * corresponding to missing argument's type.
+ * You can fill the 'missing argument' spot with {@link #__()}
+ * (which is the factory function for {@link ArgumentPlaceholder})
+ *
+ * NOTE: It is highly recommended to <b>only</b> use {@code ClassName::methodName}
+ * (aka unbounded method references) as the 1st argument for any of the
+ * factories ({@code obtain*(...)}) to avoid unwanted allocations.
+ * This means <b>not</b> using:
+ * <ul>
+ * <li>{@code someVar::methodName} or {@code this::methodName} as it captures the reference
+ * on the left of {@code ::}, resulting in an allocation on each evaluation of such
+ * bounded method references</li>
+ *
+ * <li>A lambda expression, e.g. {@code () -> toString()} due to how easy it is to accidentally
+ * capture state from outside. In the above lambda expression for example, no variable from
+ * outer scope is explicitly mentioned, yet one is still captured due to {@code toString()}
+ * being an equivalent of {@code this.toString()}</li>
+ * </ul>
+ *
+ * @hide
+ */
+@SuppressWarnings({"unchecked", "unused", "WeakerAccess"})
+public interface PooledLambda {
+
+ /**
+ * Recycles this instance. No-op if already recycled.
+ */
+ void recycle();
+
+ /**
+ * Makes this instance automatically {@link #recycle} itself after the first call.
+ *
+ * @return this instance for convenience
+ */
+ PooledLambda recycleOnUse();
+
+
+ // Factories
+
+ /**
+ * @return {@link ArgumentPlaceholder} with the inferred type parameter value
+ */
+ static <R> ArgumentPlaceholder<R> __() {
+ return (ArgumentPlaceholder<R>) ArgumentPlaceholder.INSTANCE;
+ }
+
+ /**
+ * @param typeHint the explicitly specified type of the missing argument
+ * @return {@link ArgumentPlaceholder} with the specified type parameter value
+ */
+ static <R> ArgumentPlaceholder<R> __(Class<R> typeHint) {
+ return __();
+ }
+
+ /**
+ * Wraps the given value into a {@link PooledSupplier}
+ *
+ * @param value a value to wrap
+ * @return a pooled supplier of {@code value}
+ */
+ static <R> PooledSupplier<R> obtainSupplier(R value) {
+ PooledLambdaImpl r = acquireConstSupplier(ReturnType.OBJECT);
+ r.mFunc = value;
+ return r;
+ }
+
+ /**
+ * Wraps the given value into a {@link PooledSupplier}
+ *
+ * @param value a value to wrap
+ * @return a pooled supplier of {@code value}
+ */
+ static PooledSupplier.OfInt obtainSupplier(int value) {
+ PooledLambdaImpl r = acquireConstSupplier(ReturnType.INT);
+ r.mConstValue = value;
+ return r;
+ }
+
+ /**
+ * Wraps the given value into a {@link PooledSupplier}
+ *
+ * @param value a value to wrap
+ * @return a pooled supplier of {@code value}
+ */
+ static PooledSupplier.OfLong obtainSupplier(long value) {
+ PooledLambdaImpl r = acquireConstSupplier(ReturnType.LONG);
+ r.mConstValue = value;
+ return r;
+ }
+
+ /**
+ * Wraps the given value into a {@link PooledSupplier}
+ *
+ * @param value a value to wrap
+ * @return a pooled supplier of {@code value}
+ */
+ static PooledSupplier.OfDouble obtainSupplier(double value) {
+ PooledLambdaImpl r = acquireConstSupplier(ReturnType.DOUBLE);
+ r.mConstValue = Double.doubleToRawLongBits(value);
+ return r;
+ }
+
+ /**
+ * {@link PooledRunnable} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @return a {@link PooledRunnable}, equivalent to lambda:
+ * {@code () -> function(arg1) }
+ */
+ static <A> PooledRunnable obtainRunnable(
+ Consumer<? super A> function,
+ A arg1) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 1, 0, ReturnType.VOID, arg1, null, null, null);
+ }
+
+ /**
+ * {@link PooledSupplier} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @return a {@link PooledSupplier}, equivalent to lambda:
+ * {@code () -> function(arg1) }
+ */
+ static <A> PooledSupplier<Boolean> obtainSupplier(
+ Predicate<? super A> function,
+ A arg1) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 1, 0, ReturnType.BOOLEAN, arg1, null, null, null);
+ }
+
+ /**
+ * {@link PooledSupplier} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @return a {@link PooledSupplier}, equivalent to lambda:
+ * {@code () -> function(arg1) }
+ */
+ static <A, R> PooledSupplier<R> obtainSupplier(
+ Function<? super A, ? extends R> function,
+ A arg1) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 1, 0, ReturnType.OBJECT, arg1, null, null, null);
+ }
+
+ /**
+ * Factory of {@link Message}s that contain an
+ * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
+ * {@link Message#getCallback internal callback}.
+ *
+ * The callback is equivalent to one obtainable via
+ * {@link #obtainRunnable(Consumer, Object)}
+ *
+ * Note that using this method with {@link android.os.Handler#handleMessage}
+ * is more efficient than the alternative of {@link android.os.Handler#post}
+ * with a {@link PooledRunnable} due to the lack of 2 separate synchronization points
+ * when obtaining {@link Message} and {@link PooledRunnable} from pools separately
+ *
+ * You may optionally set a {@link Message#what} for the message if you want to be
+ * able to cancel it via {@link android.os.Handler#removeMessages}, but otherwise
+ * there's no need to do so
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @return a {@link Message} invoking {@code function(arg1) } when handled
+ */
+ static <A> Message obtainMessage(
+ Consumer<? super A> function,
+ A arg1) {
+ synchronized (Message.sPoolSync) {
+ PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool,
+ function, 1, 0, ReturnType.VOID, arg1, null, null, null);
+ return Message.obtain().setCallback(callback.recycleOnUse());
+ }
+ }
+
+ /**
+ * {@link PooledRunnable} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @return a {@link PooledRunnable}, equivalent to lambda:
+ * {@code () -> function(arg1, arg2) }
+ */
+ static <A, B> PooledRunnable obtainRunnable(
+ BiConsumer<? super A, ? super B> function,
+ A arg1, B arg2) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 2, 0, ReturnType.VOID, arg1, arg2, null, null);
+ }
+
+ /**
+ * {@link PooledSupplier} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @return a {@link PooledSupplier}, equivalent to lambda:
+ * {@code () -> function(arg1, arg2) }
+ */
+ static <A, B> PooledSupplier<Boolean> obtainSupplier(
+ BiPredicate<? super A, ? super B> function,
+ A arg1, B arg2) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 2, 0, ReturnType.BOOLEAN, arg1, arg2, null, null);
+ }
+
+ /**
+ * {@link PooledSupplier} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @return a {@link PooledSupplier}, equivalent to lambda:
+ * {@code () -> function(arg1, arg2) }
+ */
+ static <A, B, R> PooledSupplier<R> obtainSupplier(
+ BiFunction<? super A, ? super B, ? extends R> function,
+ A arg1, B arg2) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 2, 0, ReturnType.OBJECT, arg1, arg2, null, null);
+ }
+
+ /**
+ * {@link PooledConsumer} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg2 parameter supplied to {@code function} on call
+ * @return a {@link PooledConsumer}, equivalent to lambda:
+ * {@code (arg1) -> function(arg1, arg2) }
+ */
+ static <A, B> PooledConsumer<A> obtainConsumer(
+ BiConsumer<? super A, ? super B> function,
+ ArgumentPlaceholder<A> arg1, B arg2) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 2, 1, ReturnType.VOID, arg1, arg2, null, null);
+ }
+
+ /**
+ * {@link PooledPredicate} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg2 parameter supplied to {@code function} on call
+ * @return a {@link PooledPredicate}, equivalent to lambda:
+ * {@code (arg1) -> function(arg1, arg2) }
+ */
+ static <A, B> PooledPredicate<A> obtainPredicate(
+ BiPredicate<? super A, ? super B> function,
+ ArgumentPlaceholder<A> arg1, B arg2) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 2, 1, ReturnType.BOOLEAN, arg1, arg2, null, null);
+ }
+
+ /**
+ * {@link PooledFunction} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg2 parameter supplied to {@code function} on call
+ * @return a {@link PooledFunction}, equivalent to lambda:
+ * {@code (arg1) -> function(arg1, arg2) }
+ */
+ static <A, B, R> PooledFunction<A, R> obtainFunction(
+ BiFunction<? super A, ? super B, ? extends R> function,
+ ArgumentPlaceholder<A> arg1, B arg2) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 2, 1, ReturnType.OBJECT, arg1, arg2, null, null);
+ }
+
+ /**
+ * {@link PooledConsumer} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
+ * @return a {@link PooledConsumer}, equivalent to lambda:
+ * {@code (arg2) -> function(arg1, arg2) }
+ */
+ static <A, B> PooledConsumer<B> obtainConsumer(
+ BiConsumer<? super A, ? super B> function,
+ A arg1, ArgumentPlaceholder<B> arg2) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 2, 1, ReturnType.VOID, arg1, arg2, null, null);
+ }
+
+ /**
+ * {@link PooledPredicate} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
+ * @return a {@link PooledPredicate}, equivalent to lambda:
+ * {@code (arg2) -> function(arg1, arg2) }
+ */
+ static <A, B> PooledPredicate<B> obtainPredicate(
+ BiPredicate<? super A, ? super B> function,
+ A arg1, ArgumentPlaceholder<B> arg2) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 2, 1, ReturnType.BOOLEAN, arg1, arg2, null, null);
+ }
+
+ /**
+ * {@link PooledFunction} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
+ * @return a {@link PooledFunction}, equivalent to lambda:
+ * {@code (arg2) -> function(arg1, arg2) }
+ */
+ static <A, B, R> PooledFunction<B, R> obtainFunction(
+ BiFunction<? super A, ? super B, ? extends R> function,
+ A arg1, ArgumentPlaceholder<B> arg2) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 2, 1, ReturnType.OBJECT, arg1, arg2, null, null);
+ }
+
+ /**
+ * Factory of {@link Message}s that contain an
+ * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
+ * {@link Message#getCallback internal callback}.
+ *
+ * The callback is equivalent to one obtainable via
+ * {@link #obtainRunnable(BiConsumer, Object, Object)}
+ *
+ * Note that using this method with {@link android.os.Handler#handleMessage}
+ * is more efficient than the alternative of {@link android.os.Handler#post}
+ * with a {@link PooledRunnable} due to the lack of 2 separate synchronization points
+ * when obtaining {@link Message} and {@link PooledRunnable} from pools separately
+ *
+ * You may optionally set a {@link Message#what} for the message if you want to be
+ * able to cancel it via {@link android.os.Handler#removeMessages}, but otherwise
+ * there's no need to do so
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @return a {@link Message} invoking {@code function(arg1, arg2) } when handled
+ */
+ static <A, B> Message obtainMessage(
+ BiConsumer<? super A, ? super B> function,
+ A arg1, B arg2) {
+ synchronized (Message.sPoolSync) {
+ PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool,
+ function, 2, 0, ReturnType.VOID, arg1, arg2, null, null);
+ return Message.obtain().setCallback(callback.recycleOnUse());
+ }
+ }
+
+ /**
+ * {@link PooledRunnable} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @return a {@link PooledRunnable}, equivalent to lambda:
+ * {@code () -> function(arg1, arg2, arg3) }
+ */
+ static <A, B, C> PooledRunnable obtainRunnable(
+ TriConsumer<? super A, ? super B, ? super C> function,
+ A arg1, B arg2, C arg3) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 3, 0, ReturnType.VOID, arg1, arg2, arg3, null);
+ }
+
+ /**
+ * {@link PooledSupplier} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @return a {@link PooledSupplier}, equivalent to lambda:
+ * {@code () -> function(arg1, arg2, arg3) }
+ */
+ static <A, B, C, R> PooledSupplier<R> obtainSupplier(
+ TriFunction<? super A, ? super B, ? super C, ? extends R> function,
+ A arg1, B arg2, C arg3) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 3, 0, ReturnType.OBJECT, arg1, arg2, arg3, null);
+ }
+
+ /**
+ * {@link PooledConsumer} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @return a {@link PooledConsumer}, equivalent to lambda:
+ * {@code (arg1) -> function(arg1, arg2, arg3) }
+ */
+ static <A, B, C> PooledConsumer<A> obtainConsumer(
+ TriConsumer<? super A, ? super B, ? super C> function,
+ ArgumentPlaceholder<A> arg1, B arg2, C arg3) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 3, 1, ReturnType.VOID, arg1, arg2, arg3, null);
+ }
+
+ /**
+ * {@link PooledFunction} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @return a {@link PooledFunction}, equivalent to lambda:
+ * {@code (arg1) -> function(arg1, arg2, arg3) }
+ */
+ static <A, B, C, R> PooledFunction<A, R> obtainFunction(
+ TriFunction<? super A, ? super B, ? super C, ? extends R> function,
+ ArgumentPlaceholder<A> arg1, B arg2, C arg3) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 3, 1, ReturnType.OBJECT, arg1, arg2, arg3, null);
+ }
+
+ /**
+ * {@link PooledConsumer} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg3 parameter supplied to {@code function} on call
+ * @return a {@link PooledConsumer}, equivalent to lambda:
+ * {@code (arg2) -> function(arg1, arg2, arg3) }
+ */
+ static <A, B, C> PooledConsumer<B> obtainConsumer(
+ TriConsumer<? super A, ? super B, ? super C> function,
+ A arg1, ArgumentPlaceholder<B> arg2, C arg3) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 3, 1, ReturnType.VOID, arg1, arg2, arg3, null);
+ }
+
+ /**
+ * {@link PooledFunction} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg3 parameter supplied to {@code function} on call
+ * @return a {@link PooledFunction}, equivalent to lambda:
+ * {@code (arg2) -> function(arg1, arg2, arg3) }
+ */
+ static <A, B, C, R> PooledFunction<B, R> obtainFunction(
+ TriFunction<? super A, ? super B, ? super C, ? extends R> function,
+ A arg1, ArgumentPlaceholder<B> arg2, C arg3) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 3, 1, ReturnType.OBJECT, arg1, arg2, arg3, null);
+ }
+
+ /**
+ * {@link PooledConsumer} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 placeholder for a missing argument. Use {@link #__} to get one
+ * @return a {@link PooledConsumer}, equivalent to lambda:
+ * {@code (arg3) -> function(arg1, arg2, arg3) }
+ */
+ static <A, B, C> PooledConsumer<C> obtainConsumer(
+ TriConsumer<? super A, ? super B, ? super C> function,
+ A arg1, B arg2, ArgumentPlaceholder<C> arg3) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 3, 1, ReturnType.VOID, arg1, arg2, arg3, null);
+ }
+
+ /**
+ * {@link PooledFunction} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 placeholder for a missing argument. Use {@link #__} to get one
+ * @return a {@link PooledFunction}, equivalent to lambda:
+ * {@code (arg3) -> function(arg1, arg2, arg3) }
+ */
+ static <A, B, C, R> PooledFunction<C, R> obtainFunction(
+ TriFunction<? super A, ? super B, ? super C, ? extends R> function,
+ A arg1, B arg2, ArgumentPlaceholder<C> arg3) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 3, 1, ReturnType.OBJECT, arg1, arg2, arg3, null);
+ }
+
+ /**
+ * Factory of {@link Message}s that contain an
+ * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
+ * {@link Message#getCallback internal callback}.
+ *
+ * The callback is equivalent to one obtainable via
+ * {@link #obtainRunnable(TriConsumer, Object, Object, Object)}
+ *
+ * Note that using this method with {@link android.os.Handler#handleMessage}
+ * is more efficient than the alternative of {@link android.os.Handler#post}
+ * with a {@link PooledRunnable} due to the lack of 2 separate synchronization points
+ * when obtaining {@link Message} and {@link PooledRunnable} from pools separately
+ *
+ * You may optionally set a {@link Message#what} for the message if you want to be
+ * able to cancel it via {@link android.os.Handler#removeMessages}, but otherwise
+ * there's no need to do so
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @return a {@link Message} invoking {@code function(arg1, arg2, arg3) } when handled
+ */
+ static <A, B, C> Message obtainMessage(
+ TriConsumer<? super A, ? super B, ? super C> function,
+ A arg1, B arg2, C arg3) {
+ synchronized (Message.sPoolSync) {
+ PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool,
+ function, 3, 0, ReturnType.VOID, arg1, arg2, arg3, null);
+ return Message.obtain().setCallback(callback.recycleOnUse());
+ }
+ }
+
+ /**
+ * {@link PooledRunnable} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @param arg4 parameter supplied to {@code function} on call
+ * @return a {@link PooledRunnable}, equivalent to lambda:
+ * {@code () -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D> PooledRunnable obtainRunnable(
+ QuadConsumer<? super A, ? super B, ? super C, ? super D> function,
+ A arg1, B arg2, C arg3, D arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 0, ReturnType.VOID, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * {@link PooledSupplier} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @param arg4 parameter supplied to {@code function} on call
+ * @return a {@link PooledSupplier}, equivalent to lambda:
+ * {@code () -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D, R> PooledSupplier<R> obtainSupplier(
+ QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
+ A arg1, B arg2, C arg3, D arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * {@link PooledConsumer} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @param arg4 parameter supplied to {@code function} on call
+ * @return a {@link PooledConsumer}, equivalent to lambda:
+ * {@code (arg1) -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D> PooledConsumer<A> obtainConsumer(
+ QuadConsumer<? super A, ? super B, ? super C, ? super D> function,
+ ArgumentPlaceholder<A> arg1, B arg2, C arg3, D arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 1, ReturnType.VOID, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * {@link PooledFunction} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @param arg4 parameter supplied to {@code function} on call
+ * @return a {@link PooledFunction}, equivalent to lambda:
+ * {@code (arg1) -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D, R> PooledFunction<A, R> obtainFunction(
+ QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
+ ArgumentPlaceholder<A> arg1, B arg2, C arg3, D arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * {@link PooledConsumer} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg3 parameter supplied to {@code function} on call
+ * @param arg4 parameter supplied to {@code function} on call
+ * @return a {@link PooledConsumer}, equivalent to lambda:
+ * {@code (arg2) -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D> PooledConsumer<B> obtainConsumer(
+ QuadConsumer<? super A, ? super B, ? super C, ? super D> function,
+ A arg1, ArgumentPlaceholder<B> arg2, C arg3, D arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 1, ReturnType.VOID, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * {@link PooledFunction} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg3 parameter supplied to {@code function} on call
+ * @param arg4 parameter supplied to {@code function} on call
+ * @return a {@link PooledFunction}, equivalent to lambda:
+ * {@code (arg2) -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D, R> PooledFunction<B, R> obtainFunction(
+ QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
+ A arg1, ArgumentPlaceholder<B> arg2, C arg3, D arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * {@link PooledConsumer} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg4 parameter supplied to {@code function} on call
+ * @return a {@link PooledConsumer}, equivalent to lambda:
+ * {@code (arg3) -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D> PooledConsumer<C> obtainConsumer(
+ QuadConsumer<? super A, ? super B, ? super C, ? super D> function,
+ A arg1, B arg2, ArgumentPlaceholder<C> arg3, D arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 1, ReturnType.VOID, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * {@link PooledFunction} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 placeholder for a missing argument. Use {@link #__} to get one
+ * @param arg4 parameter supplied to {@code function} on call
+ * @return a {@link PooledFunction}, equivalent to lambda:
+ * {@code (arg3) -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D, R> PooledFunction<C, R> obtainFunction(
+ QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
+ A arg1, B arg2, ArgumentPlaceholder<C> arg3, D arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * {@link PooledConsumer} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @param arg4 placeholder for a missing argument. Use {@link #__} to get one
+ * @return a {@link PooledConsumer}, equivalent to lambda:
+ * {@code (arg4) -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D> PooledConsumer<D> obtainConsumer(
+ QuadConsumer<? super A, ? super B, ? super C, ? super D> function,
+ A arg1, B arg2, C arg3, ArgumentPlaceholder<D> arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 1, ReturnType.VOID, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * {@link PooledFunction} factory
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @param arg4 placeholder for a missing argument. Use {@link #__} to get one
+ * @return a {@link PooledFunction}, equivalent to lambda:
+ * {@code (arg4) -> function(arg1, arg2, arg3, arg4) }
+ */
+ static <A, B, C, D, R> PooledFunction<D, R> obtainFunction(
+ QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
+ A arg1, B arg2, C arg3, ArgumentPlaceholder<D> arg4) {
+ return acquire(PooledLambdaImpl.sPool,
+ function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4);
+ }
+
+ /**
+ * Factory of {@link Message}s that contain an
+ * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
+ * {@link Message#getCallback internal callback}.
+ *
+ * The callback is equivalent to one obtainable via
+ * {@link #obtainRunnable(QuadConsumer, Object, Object, Object, Object)}
+ *
+ * Note that using this method with {@link android.os.Handler#handleMessage}
+ * is more efficient than the alternative of {@link android.os.Handler#post}
+ * with a {@link PooledRunnable} due to the lack of 2 separate synchronization points
+ * when obtaining {@link Message} and {@link PooledRunnable} from pools separately
+ *
+ * You may optionally set a {@link Message#what} for the message if you want to be
+ * able to cancel it via {@link android.os.Handler#removeMessages}, but otherwise
+ * there's no need to do so
+ *
+ * @param function non-capturing lambda(typically an unbounded method reference)
+ * to be invoked on call
+ * @param arg1 parameter supplied to {@code function} on call
+ * @param arg2 parameter supplied to {@code function} on call
+ * @param arg3 parameter supplied to {@code function} on call
+ * @param arg4 parameter supplied to {@code function} on call
+ * @return a {@link Message} invoking {@code function(arg1, arg2, arg3, arg4) } when handled
+ */
+ static <A, B, C, D> Message obtainMessage(
+ QuadConsumer<? super A, ? super B, ? super C, ? super D> function,
+ A arg1, B arg2, C arg3, D arg4) {
+ synchronized (Message.sPoolSync) {
+ PooledRunnable callback = acquire(PooledLambdaImpl.sMessageCallbacksPool,
+ function, 4, 0, ReturnType.VOID, arg1, arg2, arg3, arg4);
+ return Message.obtain().setCallback(callback.recycleOnUse());
+ }
+ }
+}
diff --git a/com/android/internal/util/function/pooled/PooledLambdaImpl.java b/com/android/internal/util/function/pooled/PooledLambdaImpl.java
new file mode 100644
index 00000000..03e013cd
--- /dev/null
+++ b/com/android/internal/util/function/pooled/PooledLambdaImpl.java
@@ -0,0 +1,557 @@
+/*
+ * 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 com.android.internal.util.function.pooled;
+
+import android.annotation.Nullable;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pools;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.BitUtils;
+import com.android.internal.util.function.QuadConsumer;
+import com.android.internal.util.function.QuadFunction;
+import com.android.internal.util.function.QuadPredicate;
+import com.android.internal.util.function.TriConsumer;
+import com.android.internal.util.function.TriFunction;
+import com.android.internal.util.function.TriPredicate;
+
+import java.util.Arrays;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * @see PooledLambda
+ * @hide
+ */
+final class PooledLambdaImpl<R> extends OmniFunction<Object, Object, Object, Object, R> {
+
+ private static final boolean DEBUG = false;
+ private static final String LOG_TAG = "PooledLambdaImpl";
+
+ private static final int MAX_ARGS = 4;
+
+ private static final int MAX_POOL_SIZE = 50;
+
+ static class Pool extends Pools.SynchronizedPool<PooledLambdaImpl> {
+
+ public Pool(Object lock) {
+ super(MAX_POOL_SIZE, lock);
+ }
+ }
+
+ static final Pool sPool = new Pool(new Object());
+ static final Pool sMessageCallbacksPool = new Pool(Message.sPoolSync);
+
+ private PooledLambdaImpl() {}
+
+ /**
+ * The function reference to be invoked
+ *
+ * May be the return value itself in case when an immediate result constant is provided instead
+ */
+ Object mFunc;
+
+ /**
+ * A primitive result value to be immediately returned on invocation instead of calling
+ * {@link #mFunc}
+ */
+ long mConstValue;
+
+ /**
+ * Arguments for {@link #mFunc}
+ */
+ @Nullable Object[] mArgs = null;
+
+ /**
+ * Flag for {@link #mFlags}
+ *
+ * Indicates whether this instance is recycled
+ */
+ private static final int FLAG_RECYCLED = 1 << MAX_ARGS;
+
+ /**
+ * Flag for {@link #mFlags}
+ *
+ * Indicates whether this instance should be immediately recycled on invocation
+ * (as requested via {@link PooledLambda#recycleOnUse()}) or not(default)
+ */
+ private static final int FLAG_RECYCLE_ON_USE = 1 << (MAX_ARGS + 1);
+
+ /**
+ * Flag for {@link #mFlags}
+ *
+ * Indicates that this instance was acquired from {@link #sMessageCallbacksPool} as opposed to
+ * {@link #sPool}
+ */
+ private static final int FLAG_ACQUIRED_FROM_MESSAGE_CALLBACKS_POOL = 1 << (MAX_ARGS + 2);
+
+ /** @see #mFlags */
+ static final int MASK_EXPOSED_AS = LambdaType.MASK << (MAX_ARGS + 3);
+
+ /** @see #mFlags */
+ static final int MASK_FUNC_TYPE = LambdaType.MASK <<
+ (MAX_ARGS + 3 + LambdaType.MASK_BIT_COUNT);
+
+ /**
+ * Bit schema:
+ * AAAABCDEEEEEEFFFFFF
+ *
+ * Where:
+ * A - whether {@link #mArgs arg} at corresponding index was specified at
+ * {@link #acquire creation time} (0) or {@link #invoke invocation time} (1)
+ * B - {@link #FLAG_RECYCLED}
+ * C - {@link #FLAG_RECYCLE_ON_USE}
+ * D - {@link #FLAG_ACQUIRED_FROM_MESSAGE_CALLBACKS_POOL}
+ * E - {@link LambdaType} representing the type of the lambda returned to the caller from a
+ * factory method
+ * F - {@link LambdaType} of {@link #mFunc} as resolved when calling a factory method
+ */
+ int mFlags = 0;
+
+
+ @Override
+ public void recycle() {
+ if (DEBUG) Log.i(LOG_TAG, this + ".recycle()");
+ if (!isRecycled()) doRecycle();
+ }
+
+ private void doRecycle() {
+ if (DEBUG) Log.i(LOG_TAG, this + ".doRecycle()");
+ Pool pool = (mFlags & FLAG_ACQUIRED_FROM_MESSAGE_CALLBACKS_POOL) != 0
+ ? PooledLambdaImpl.sMessageCallbacksPool
+ : PooledLambdaImpl.sPool;
+
+ mFunc = null;
+ if (mArgs != null) Arrays.fill(mArgs, null);
+ mFlags = FLAG_RECYCLED;
+ mConstValue = 0L;
+
+ pool.release(this);
+ }
+
+ @Override
+ R invoke(Object a1, Object a2, Object a3, Object a4) {
+ checkNotRecycled();
+ if (DEBUG) {
+ Log.i(LOG_TAG, this + ".invoke("
+ + commaSeparateFirstN(
+ new Object[] { a1, a2, a3, a4 },
+ LambdaType.decodeArgCount(getFlags(MASK_EXPOSED_AS)))
+ + ")");
+ }
+ boolean ignored = fillInArg(a1) && fillInArg(a2) && fillInArg(a3) && fillInArg(a4);
+ int argCount = LambdaType.decodeArgCount(getFlags(MASK_FUNC_TYPE));
+ if (argCount != LambdaType.MASK_ARG_COUNT) {
+ for (int i = 0; i < argCount; i++) {
+ if (mArgs[i] == ArgumentPlaceholder.INSTANCE) {
+ throw new IllegalStateException("Missing argument #" + i + " among "
+ + Arrays.toString(mArgs));
+ }
+ }
+ }
+ try {
+ return doInvoke();
+ } finally {
+ if (isRecycleOnUse()) doRecycle();
+ if (!isRecycled()) {
+ int argsSize = ArrayUtils.size(mArgs);
+ for (int i = 0; i < argsSize; i++) {
+ popArg(i);
+ }
+ }
+ }
+ }
+
+ private boolean fillInArg(Object invocationArg) {
+ int argsSize = ArrayUtils.size(mArgs);
+ for (int i = 0; i < argsSize; i++) {
+ if (mArgs[i] == ArgumentPlaceholder.INSTANCE) {
+ mArgs[i] = invocationArg;
+ mFlags |= BitUtils.bitAt(i);
+ return true;
+ }
+ }
+ if (invocationArg != null && invocationArg != ArgumentPlaceholder.INSTANCE) {
+ throw new IllegalStateException("No more arguments expected for provided arg "
+ + invocationArg + " among " + Arrays.toString(mArgs));
+ }
+ return false;
+ }
+
+ private void checkNotRecycled() {
+ if (isRecycled()) throw new IllegalStateException("Instance is recycled: " + this);
+ }
+
+ @SuppressWarnings("unchecked")
+ private R doInvoke() {
+ final int funcType = getFlags(MASK_FUNC_TYPE);
+ final int argCount = LambdaType.decodeArgCount(funcType);
+ final int returnType = LambdaType.decodeReturnType(funcType);
+
+ switch (argCount) {
+ case LambdaType.MASK_ARG_COUNT: {
+ switch (returnType) {
+ case LambdaType.ReturnType.INT: return (R) (Integer) getAsInt();
+ case LambdaType.ReturnType.LONG: return (R) (Long) getAsLong();
+ case LambdaType.ReturnType.DOUBLE: return (R) (Double) getAsDouble();
+ default: return (R) mFunc;
+ }
+ }
+ case 0: {
+ switch (returnType) {
+ case LambdaType.ReturnType.VOID: {
+ ((Runnable) mFunc).run();
+ return null;
+ }
+ case LambdaType.ReturnType.BOOLEAN:
+ case LambdaType.ReturnType.OBJECT: {
+ return (R) ((Supplier) mFunc).get();
+ }
+ }
+ } break;
+ case 1: {
+ switch (returnType) {
+ case LambdaType.ReturnType.VOID: {
+ ((Consumer) mFunc).accept(popArg(0));
+ return null;
+ }
+ case LambdaType.ReturnType.BOOLEAN: {
+ return (R) (Object) ((Predicate) mFunc).test(popArg(0));
+ }
+ case LambdaType.ReturnType.OBJECT: {
+ return (R) ((Function) mFunc).apply(popArg(0));
+ }
+ }
+ } break;
+ case 2: {
+ switch (returnType) {
+ case LambdaType.ReturnType.VOID: {
+ ((BiConsumer) mFunc).accept(popArg(0), popArg(1));
+ return null;
+ }
+ case LambdaType.ReturnType.BOOLEAN: {
+ return (R) (Object) ((BiPredicate) mFunc).test(popArg(0), popArg(1));
+ }
+ case LambdaType.ReturnType.OBJECT: {
+ return (R) ((BiFunction) mFunc).apply(popArg(0), popArg(1));
+ }
+ }
+ } break;
+ case 3: {
+ switch (returnType) {
+ case LambdaType.ReturnType.VOID: {
+ ((TriConsumer) mFunc).accept(popArg(0), popArg(1), popArg(2));
+ return null;
+ }
+ case LambdaType.ReturnType.BOOLEAN: {
+ return (R) (Object) ((TriPredicate) mFunc).test(
+ popArg(0), popArg(1), popArg(2));
+ }
+ case LambdaType.ReturnType.OBJECT: {
+ return (R) ((TriFunction) mFunc).apply(popArg(0), popArg(1), popArg(2));
+ }
+ }
+ } break;
+ case 4: {
+ switch (returnType) {
+ case LambdaType.ReturnType.VOID: {
+ ((QuadConsumer) mFunc).accept(popArg(0), popArg(1), popArg(2), popArg(3));
+ return null;
+ }
+ case LambdaType.ReturnType.BOOLEAN: {
+ return (R) (Object) ((QuadPredicate) mFunc).test(
+ popArg(0), popArg(1), popArg(2), popArg(3));
+ }
+ case LambdaType.ReturnType.OBJECT: {
+ return (R) ((QuadFunction) mFunc).apply(
+ popArg(0), popArg(1), popArg(2), popArg(3));
+ }
+ }
+ } break;
+ }
+ throw new IllegalStateException("Unknown function type: " + LambdaType.toString(funcType));
+ }
+
+ private boolean isConstSupplier() {
+ return LambdaType.decodeArgCount(getFlags(MASK_FUNC_TYPE)) == LambdaType.MASK_ARG_COUNT;
+ }
+
+ private Object popArg(int index) {
+ Object result = mArgs[index];
+ if (isInvocationArgAtIndex(index)) {
+ mArgs[index] = ArgumentPlaceholder.INSTANCE;
+ mFlags &= ~BitUtils.bitAt(index);
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ if (isRecycled()) return "<recycled PooledLambda@" + hashCodeHex(this) + ">";
+
+ StringBuilder sb = new StringBuilder();
+ if (isConstSupplier()) {
+ sb.append(getFuncTypeAsString()).append("(").append(doInvoke()).append(")");
+ } else {
+ if (mFunc instanceof PooledLambdaImpl) {
+ sb.append(mFunc);
+ } else {
+ sb.append(getFuncTypeAsString()).append("@").append(hashCodeHex(mFunc));
+ }
+ sb.append("(");
+ sb.append(commaSeparateFirstN(mArgs, LambdaType.decodeArgCount(getFlags(MASK_FUNC_TYPE))));
+ sb.append(")");
+ }
+ return sb.toString();
+ }
+
+ private String commaSeparateFirstN(@Nullable Object[] arr, int n) {
+ if (arr == null) return "";
+ return TextUtils.join(",", Arrays.copyOf(arr, n));
+ }
+
+ private static String hashCodeHex(Object o) {
+ return Integer.toHexString(o.hashCode());
+ }
+
+ private String getFuncTypeAsString() {
+ if (isRecycled()) throw new IllegalStateException();
+ if (isConstSupplier()) return "supplier";
+ String name = LambdaType.toString(getFlags(MASK_EXPOSED_AS));
+ if (name.endsWith("Consumer")) return "consumer";
+ if (name.endsWith("Function")) return "function";
+ if (name.endsWith("Predicate")) return "predicate";
+ if (name.endsWith("Supplier")) return "supplier";
+ if (name.endsWith("Runnable")) return "runnable";
+ throw new IllegalStateException("Don't know the string representation of " + name);
+ }
+
+ /**
+ * Internal non-typesafe factory method for {@link PooledLambdaImpl}
+ */
+ static <E extends PooledLambda> E acquire(Pool pool, Object f,
+ int fNumArgs, int numPlaceholders, int fReturnType,
+ Object a, Object b, Object c, Object d) {
+ PooledLambdaImpl r = acquire(pool);
+ if (DEBUG) {
+ Log.i(LOG_TAG,
+ "acquire(this = @" + hashCodeHex(r)
+ + ", f = " + f
+ + ", fNumArgs = " + fNumArgs
+ + ", numPlaceholders = " + numPlaceholders
+ + ", fReturnType = " + LambdaType.ReturnType.toString(fReturnType)
+ + ", a = " + a
+ + ", b = " + b
+ + ", c = " + c
+ + ", d = " + d
+ + ")");
+ }
+ r.mFunc = f;
+ r.setFlags(MASK_FUNC_TYPE, LambdaType.encode(fNumArgs, fReturnType));
+ r.setFlags(MASK_EXPOSED_AS, LambdaType.encode(numPlaceholders, fReturnType));
+ if (ArrayUtils.size(r.mArgs) < fNumArgs) r.mArgs = new Object[fNumArgs];
+ setIfInBounds(r.mArgs, 0, a);
+ setIfInBounds(r.mArgs, 1, b);
+ setIfInBounds(r.mArgs, 2, c);
+ setIfInBounds(r.mArgs, 3, d);
+ return (E) r;
+ }
+
+ static PooledLambdaImpl acquireConstSupplier(int type) {
+ PooledLambdaImpl r = acquire(PooledLambdaImpl.sPool);
+ int lambdaType = LambdaType.encode(LambdaType.MASK_ARG_COUNT, type);
+ r.setFlags(PooledLambdaImpl.MASK_FUNC_TYPE, lambdaType);
+ r.setFlags(PooledLambdaImpl.MASK_EXPOSED_AS, lambdaType);
+ return r;
+ }
+
+ static PooledLambdaImpl acquire(Pool pool) {
+ PooledLambdaImpl r = pool.acquire();
+ if (r == null) r = new PooledLambdaImpl();
+ r.mFlags &= ~FLAG_RECYCLED;
+ r.setFlags(FLAG_ACQUIRED_FROM_MESSAGE_CALLBACKS_POOL,
+ pool == sMessageCallbacksPool ? 1 : 0);
+ return r;
+ }
+
+ private static void setIfInBounds(Object[] array, int i, Object a) {
+ if (i < ArrayUtils.size(array)) array[i] = a;
+ }
+
+ @Override
+ public OmniFunction<Object, Object, Object, Object, R> negate() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <V> OmniFunction<Object, Object, Object, Object, V> andThen(
+ Function<? super R, ? extends V> after) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public double getAsDouble() {
+ return Double.longBitsToDouble(mConstValue);
+ }
+
+ @Override
+ public int getAsInt() {
+ return (int) mConstValue;
+ }
+
+ @Override
+ public long getAsLong() {
+ return mConstValue;
+ }
+
+ @Override
+ public OmniFunction<Object, Object, Object, Object, R> recycleOnUse() {
+ if (DEBUG) Log.i(LOG_TAG, this + ".recycleOnUse()");
+ mFlags |= FLAG_RECYCLE_ON_USE;
+ return this;
+ }
+
+ private boolean isRecycled() {
+ return (mFlags & FLAG_RECYCLED) != 0;
+ }
+
+ private boolean isRecycleOnUse() {
+ return (mFlags & FLAG_RECYCLE_ON_USE) != 0;
+ }
+
+ private boolean isInvocationArgAtIndex(int argIndex) {
+ return (mFlags & (1 << argIndex)) != 0;
+ }
+
+ int getFlags(int mask) {
+ return unmask(mask, mFlags);
+ }
+
+ void setFlags(int mask, int value) {
+ mFlags &= ~mask;
+ mFlags |= mask(mask, value);
+ }
+
+ /**
+ * 0xFF000, 0xAB -> 0xAB000
+ */
+ private static int mask(int mask, int value) {
+ return (value << Integer.numberOfTrailingZeros(mask)) & mask;
+ }
+
+ /**
+ * 0xFF000, 0xAB123 -> 0xAB
+ */
+ private static int unmask(int mask, int bits) {
+ return (bits & mask) / (1 << Integer.numberOfTrailingZeros(mask));
+ }
+
+ /**
+ * Contract for encoding a supported lambda type in {@link #MASK_BIT_COUNT} bits
+ */
+ static class LambdaType {
+ public static final int MASK_ARG_COUNT = 0b111;
+ public static final int MASK_RETURN_TYPE = 0b111000;
+ public static final int MASK = MASK_ARG_COUNT | MASK_RETURN_TYPE;
+ public static final int MASK_BIT_COUNT = 6;
+
+ static int encode(int argCount, int returnType) {
+ return mask(MASK_ARG_COUNT, argCount) | mask(MASK_RETURN_TYPE, returnType);
+ }
+
+ static int decodeArgCount(int type) {
+ return type & MASK_ARG_COUNT;
+ }
+
+ static int decodeReturnType(int type) {
+ return unmask(MASK_RETURN_TYPE, type);
+ }
+
+ static String toString(int type) {
+ int argCount = decodeArgCount(type);
+ int returnType = decodeReturnType(type);
+ if (argCount == 0) {
+ if (returnType == ReturnType.VOID) return "Runnable";
+ if (returnType == ReturnType.OBJECT || returnType == ReturnType.BOOLEAN) {
+ return "Supplier";
+ }
+ }
+ return argCountPrefix(argCount) + ReturnType.lambdaSuffix(returnType);
+ }
+
+ private static String argCountPrefix(int argCount) {
+ switch (argCount) {
+ case MASK_ARG_COUNT: return "";
+ case 1: return "";
+ case 2: return "Bi";
+ case 3: return "Tri";
+ case 4: return "Quad";
+ default: throw new IllegalArgumentException("" + argCount);
+ }
+ }
+
+ static class ReturnType {
+ public static final int VOID = 1;
+ public static final int BOOLEAN = 2;
+ public static final int OBJECT = 3;
+ public static final int INT = 4;
+ public static final int LONG = 5;
+ public static final int DOUBLE = 6;
+
+ static String toString(int returnType) {
+ switch (returnType) {
+ case VOID: return "VOID";
+ case BOOLEAN: return "BOOLEAN";
+ case OBJECT: return "OBJECT";
+ case INT: return "INT";
+ case LONG: return "LONG";
+ case DOUBLE: return "DOUBLE";
+ default: return "" + returnType;
+ }
+ }
+
+ static String lambdaSuffix(int type) {
+ return prefix(type) + suffix(type);
+ }
+
+ private static String prefix(int type) {
+ switch (type) {
+ case INT: return "Int";
+ case LONG: return "Long";
+ case DOUBLE: return "Double";
+ default: return "";
+ }
+ }
+
+ private static String suffix(int type) {
+ switch (type) {
+ case VOID: return "Consumer";
+ case BOOLEAN: return "Predicate";
+ case OBJECT: return "Function";
+ default: return "Supplier";
+ }
+ }
+ }
+ }
+}
diff --git a/com/android/internal/util/function/pooled/PooledPredicate.java b/com/android/internal/util/function/pooled/PooledPredicate.java
new file mode 100644
index 00000000..9b143664
--- /dev/null
+++ b/com/android/internal/util/function/pooled/PooledPredicate.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.android.internal.util.function.pooled;
+
+import java.util.function.Predicate;
+
+/**
+ * {@link Predicate} + {@link PooledLambda}
+ *
+ * @see PooledLambda
+ * @hide
+ */
+public interface PooledPredicate<T> extends PooledLambda, Predicate<T> {
+
+ /**
+ * Ignores the result
+ */
+ PooledConsumer<T> asConsumer();
+
+ /** @inheritDoc */
+ PooledPredicate<T> recycleOnUse();
+}
diff --git a/com/android/internal/util/function/pooled/PooledRunnable.java b/com/android/internal/util/function/pooled/PooledRunnable.java
new file mode 100644
index 00000000..89ca82e2
--- /dev/null
+++ b/com/android/internal/util/function/pooled/PooledRunnable.java
@@ -0,0 +1,30 @@
+/*
+ * 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 com.android.internal.util.function.pooled;
+
+import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
+
+/**
+ * {@link Runnable} + {@link PooledLambda}
+ *
+ * @see PooledLambda
+ * @hide
+ */
+public interface PooledRunnable extends PooledLambda, Runnable, ThrowingRunnable {
+ /** @inheritDoc */
+ PooledRunnable recycleOnUse();
+}
diff --git a/com/android/internal/util/function/pooled/PooledSupplier.java b/com/android/internal/util/function/pooled/PooledSupplier.java
new file mode 100644
index 00000000..dd7f73ee
--- /dev/null
+++ b/com/android/internal/util/function/pooled/PooledSupplier.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 com.android.internal.util.function.pooled;
+
+import com.android.internal.util.FunctionalUtils.ThrowingSupplier;
+
+import java.util.function.DoubleSupplier;
+import java.util.function.IntSupplier;
+import java.util.function.LongSupplier;
+import java.util.function.Supplier;
+
+/**
+ * {@link Supplier} + {@link PooledLambda}
+ *
+ * @see PooledLambda
+ * @hide
+ */
+public interface PooledSupplier<T> extends PooledLambda, Supplier<T>, ThrowingSupplier<T> {
+
+ /**
+ * Ignores the result
+ */
+ PooledRunnable asRunnable();
+
+ /** @inheritDoc */
+ PooledSupplier<T> recycleOnUse();
+
+ /** {@link PooledLambda} + {@link IntSupplier} */
+ interface OfInt extends IntSupplier, PooledLambda {
+ /** @inheritDoc */
+ PooledSupplier.OfInt recycleOnUse();
+ }
+
+ /** {@link PooledLambda} + {@link LongSupplier} */
+ interface OfLong extends LongSupplier, PooledLambda {
+ /** @inheritDoc */
+ PooledSupplier.OfLong recycleOnUse();
+ }
+
+ /** {@link PooledLambda} + {@link DoubleSupplier} */
+ interface OfDouble extends DoubleSupplier, PooledLambda {
+ /** @inheritDoc */
+ PooledSupplier.OfDouble recycleOnUse();
+ }
+}
diff --git a/com/android/internal/view/BaseIWindow.java b/com/android/internal/view/BaseIWindow.java
index 361fd3da..7178a0d6 100644
--- a/com/android/internal/view/BaseIWindow.java
+++ b/com/android/internal/view/BaseIWindow.java
@@ -22,6 +22,7 @@ import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.MergedConfiguration;
+import android.view.DisplayCutout;
import android.view.DragEvent;
import android.view.IWindow;
import android.view.IWindowSession;
@@ -41,7 +42,8 @@ public class BaseIWindow extends IWindow.Stub {
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) {
if (reportDraw) {
try {
mSession.finishDrawing(this);
diff --git a/com/android/internal/view/InputBindResult.java b/com/android/internal/view/InputBindResult.java
index 3a3e56d5..74dbaba3 100644
--- a/com/android/internal/view/InputBindResult.java
+++ b/com/android/internal/view/InputBindResult.java
@@ -16,17 +16,134 @@
package com.android.internal.view;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.UserHandle;
import android.view.InputChannel;
+import java.lang.annotation.Retention;
+
/**
* Bundle of information returned by input method manager about a successful
* binding to an input method.
*/
public final class InputBindResult implements Parcelable {
- static final String TAG = "InputBindResult";
-
+
+ @Retention(SOURCE)
+ @IntDef({
+ ResultCode.SUCCESS_WITH_IME_SESSION,
+ ResultCode.SUCCESS_WAITING_IME_SESSION,
+ ResultCode.SUCCESS_WAITING_IME_BINDING,
+ ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
+ ResultCode.ERROR_NULL,
+ ResultCode.ERROR_NO_IME,
+ ResultCode.ERROR_INVALID_PACKAGE_NAME,
+ ResultCode.ERROR_SYSTEM_NOT_READY,
+ ResultCode.ERROR_IME_NOT_CONNECTED,
+ ResultCode.ERROR_INVALID_USER,
+ ResultCode.ERROR_NULL_EDITOR_INFO,
+ ResultCode.ERROR_NOT_IME_TARGET_WINDOW,
+ })
+ public @interface ResultCode {
+ /**
+ * Indicates that everything in this result object including {@link #method} is valid.
+ */
+ int SUCCESS_WITH_IME_SESSION = 0;
+ /**
+ * Indicates that this is a temporary binding until the
+ * {@link android.inputmethodservice.InputMethodService} (IMS) establishes a valid session
+ * to {@link com.android.server.InputMethodManagerService} (IMMS).
+ *
+ * <p>Note that in this state the IMS is already bound to IMMS but the logical session
+ * is not yet established on top of the IPC channel.</p>
+ *
+ * <p>Some of fields such as {@link #channel} is not yet available.</p>
+ *
+ * @see android.inputmethodservice.InputMethodService##onCreateInputMethodSessionInterface()
+ **/
+ int SUCCESS_WAITING_IME_SESSION = 1;
+ /**
+ * Indicates that this is a temporary binding until the
+ * {@link android.inputmethodservice.InputMethodService} (IMS) establishes a valid session
+ * to {@link com.android.server.InputMethodManagerService} (IMMS).
+ *
+ * <p>Note that in this state the IMMS has already initiated a connection to the IMS but
+ * the binding process is not completed yet.</p>
+ *
+ * <p>Some of fields such as {@link #channel} is not yet available.</p>
+ * @see android.content.ServiceConnection#onServiceConnected(ComponentName, IBinder)
+ */
+ int SUCCESS_WAITING_IME_BINDING = 2;
+ /**
+ * Indicates that this is not intended for starting input but just for reporting window
+ * focus change from the application process.
+ *
+ * <p>All other fields do not have meaningful value.</p>
+ */
+ int SUCCESS_REPORT_WINDOW_FOCUS_ONLY = 3;
+ /**
+ * Indicates somehow
+ * {@link com.android.server.InputMethodManagerService#startInputOrWindowGainedFocus} is
+ * trying to return null {@link InputBindResult}, which must never happen.
+ */
+ int ERROR_NULL = 4;
+ /**
+ * Indicates that {@link com.android.server.InputMethodManagerService} recognizes no IME.
+ */
+ int ERROR_NO_IME = 5;
+ /**
+ * Indicates that {@link android.view.inputmethod.EditorInfo#packageName} does not match
+ * the caller UID.
+ *
+ * @see android.view.inputmethod.EditorInfo#packageName
+ */
+ int ERROR_INVALID_PACKAGE_NAME = 6;
+ /**
+ * Indicates that the system is still in an early stage of the boot process and any 3rd
+ * party application is not allowed to run.
+ *
+ * @see com.android.server.SystemService#PHASE_THIRD_PARTY_APPS_CAN_START
+ */
+ int ERROR_SYSTEM_NOT_READY = 7;
+ /**
+ * Indicates that {@link com.android.server.InputMethodManagerService} tried to connect to
+ * an {@link android.inputmethodservice.InputMethodService} but failed.
+ *
+ * @see android.content.Context#bindServiceAsUser(Intent, ServiceConnection, int, UserHandle)
+ */
+ int ERROR_IME_NOT_CONNECTED = 8;
+ /**
+ * Indicates that the caller is not the foreground user (or does not have
+ * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission).
+ */
+ int ERROR_INVALID_USER = 9;
+ /**
+ * Indicates that the caller should have specified non-null
+ * {@link android.view.inputmethod.EditorInfo}.
+ */
+ int ERROR_NULL_EDITOR_INFO = 10;
+ /**
+ * Indicates that the target window the client specified cannot be the IME target right now.
+ *
+ * <p>Due to the asynchronous nature of Android OS, we cannot completely avoid this error.
+ * The client should try to restart input when its {@link android.view.Window} is focused
+ * again.</p>
+ *
+ * @see com.android.server.wm.WindowManagerService#inputMethodClientHasFocus(IInputMethodClient)
+ */
+ int ERROR_NOT_IME_TARGET_WINDOW = 11;
+ }
+
+ @ResultCode
+ public final int result;
+
/**
* The input method service.
*/
@@ -53,16 +170,19 @@ public final class InputBindResult implements Parcelable {
*/
public final int userActionNotificationSequenceNumber;
- public InputBindResult(IInputMethodSession _method, InputChannel _channel,
+ public InputBindResult(@ResultCode int _result,
+ IInputMethodSession _method, InputChannel _channel,
String _id, int _sequence, int _userActionNotificationSequenceNumber) {
+ result = _result;
method = _method;
channel = _channel;
id = _id;
sequence = _sequence;
userActionNotificationSequenceNumber = _userActionNotificationSequenceNumber;
}
-
+
InputBindResult(Parcel source) {
+ result = source.readInt();
method = IInputMethodSession.Stub.asInterface(source.readStrongBinder());
if (source.readInt() != 0) {
channel = InputChannel.CREATOR.createFromParcel(source);
@@ -76,9 +196,9 @@ public final class InputBindResult implements Parcelable {
@Override
public String toString() {
- return "InputBindResult{" + method + " " + id
- + " sequence:" + sequence
- + " userActionNotificationSequenceNumber:" + userActionNotificationSequenceNumber
+ return "InputBindResult{result=" + getResultString() + " method="+ method + " id=" + id
+ + " sequence=" + sequence
+ + " userActionNotificationSequenceNumber=" + userActionNotificationSequenceNumber
+ "}";
}
@@ -90,6 +210,7 @@ public final class InputBindResult implements Parcelable {
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(result);
dest.writeStrongInterface(method);
if (channel != null) {
dest.writeInt(1);
@@ -122,4 +243,72 @@ public final class InputBindResult implements Parcelable {
public int describeContents() {
return channel != null ? channel.describeContents() : 0;
}
+
+ public String getResultString() {
+ switch (result) {
+ case ResultCode.SUCCESS_WITH_IME_SESSION:
+ return "SUCCESS_WITH_IME_SESSION";
+ case ResultCode.SUCCESS_WAITING_IME_SESSION:
+ return "SUCCESS_WAITING_IME_SESSION";
+ case ResultCode.SUCCESS_WAITING_IME_BINDING:
+ return "SUCCESS_WAITING_IME_BINDING";
+ case ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY:
+ return "SUCCESS_REPORT_WINDOW_FOCUS_ONLY";
+ case ResultCode.ERROR_NULL:
+ return "ERROR_NULL";
+ case ResultCode.ERROR_NO_IME:
+ return "ERROR_NO_IME";
+ case ResultCode.ERROR_INVALID_PACKAGE_NAME:
+ return "ERROR_INVALID_PACKAGE_NAME";
+ case ResultCode.ERROR_SYSTEM_NOT_READY:
+ return "ERROR_SYSTEM_NOT_READY";
+ case ResultCode.ERROR_IME_NOT_CONNECTED:
+ return "ERROR_IME_NOT_CONNECTED";
+ case ResultCode.ERROR_INVALID_USER:
+ return "ERROR_INVALID_USER";
+ case ResultCode.ERROR_NULL_EDITOR_INFO:
+ return "ERROR_NULL_EDITOR_INFO";
+ case ResultCode.ERROR_NOT_IME_TARGET_WINDOW:
+ return "ERROR_NOT_IME_TARGET_WINDOW";
+ default:
+ return "Unknown(" + result + ")";
+ }
+ }
+
+ private static InputBindResult error(@ResultCode int result) {
+ return new InputBindResult(result, null, null, null, -1, -1);
+ }
+
+ /**
+ * Predefined error object for {@link ResultCode#ERROR_NULL}.
+ */
+ public static final InputBindResult NULL = error(ResultCode.ERROR_NULL);
+ /**
+ * Predefined error object for {@link ResultCode#NO_IME}.
+ */
+ public static final InputBindResult NO_IME = error(ResultCode.ERROR_NO_IME);
+ /**
+ * Predefined error object for {@link ResultCode#ERROR_INVALID_PACKAGE_NAME}.
+ */
+ public static final InputBindResult INVALID_PACKAGE_NAME =
+ error(ResultCode.ERROR_INVALID_PACKAGE_NAME);
+ /**
+ * Predefined error object for {@link ResultCode#ERROR_NULL_EDITOR_INFO}.
+ */
+ public static final InputBindResult NULL_EDITOR_INFO = error(ResultCode.ERROR_NULL_EDITOR_INFO);
+ /**
+ * Predefined error object for {@link ResultCode#ERROR_NOT_IME_TARGET_WINDOW}.
+ */
+ public static final InputBindResult NOT_IME_TARGET_WINDOW =
+ error(ResultCode.ERROR_NOT_IME_TARGET_WINDOW);
+ /**
+ * Predefined error object for {@link ResultCode#ERROR_IME_NOT_CONNECTED}.
+ */
+ public static final InputBindResult IME_NOT_CONNECTED =
+ error(ResultCode.ERROR_IME_NOT_CONNECTED);
+ /**
+ * Predefined error object for {@link ResultCode#ERROR_INVALID_USER}.
+ */
+ public static final InputBindResult INVALID_USER = error(ResultCode.ERROR_INVALID_USER);
+
}
diff --git a/com/android/internal/view/RotationPolicy.java b/com/android/internal/view/RotationPolicy.java
index b479cb1f..d7b91325 100644
--- a/com/android/internal/view/RotationPolicy.java
+++ b/com/android/internal/view/RotationPolicy.java
@@ -108,11 +108,19 @@ public final class RotationPolicy {
* Enables or disables rotation lock from the system UI toggle.
*/
public static void setRotationLock(Context context, final boolean enabled) {
+ final int rotation = areAllRotationsAllowed(context) ? CURRENT_ROTATION : NATURAL_ROTATION;
+ setRotationLockAtAngle(context, enabled, rotation);
+ }
+
+ /**
+ * Enables or disables rotation lock at a specific rotation from system UI.
+ */
+ public static void setRotationLockAtAngle(Context context, final boolean enabled,
+ final int rotation) {
Settings.System.putIntForUser(context.getContentResolver(),
Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, 0,
UserHandle.USER_CURRENT);
- final int rotation = areAllRotationsAllowed(context) ? CURRENT_ROTATION : NATURAL_ROTATION;
setRotationLock(enabled, rotation);
}
diff --git a/com/android/internal/view/menu/ListMenuItemView.java b/com/android/internal/view/menu/ListMenuItemView.java
index 8f80bfe3..0d2b29b6 100644
--- a/com/android/internal/view/menu/ListMenuItemView.java
+++ b/com/android/internal/view/menu/ListMenuItemView.java
@@ -50,6 +50,7 @@ public class ListMenuItemView extends LinearLayout
private TextView mShortcutView;
private ImageView mSubMenuArrowView;
private ImageView mGroupDivider;
+ private LinearLayout mContent;
private Drawable mBackground;
private int mTextAppearance;
@@ -114,6 +115,8 @@ public class ListMenuItemView extends LinearLayout
mSubMenuArrowView.setImageDrawable(mSubMenuArrow);
}
mGroupDivider = findViewById(com.android.internal.R.id.group_divider);
+
+ mContent = findViewById(com.android.internal.R.id.content);
}
public void initialize(MenuItemImpl itemData, int menuType) {
@@ -131,6 +134,18 @@ public class ListMenuItemView extends LinearLayout
setContentDescription(itemData.getContentDescription());
}
+ private void addContentView(View v) {
+ addContentView(v, -1);
+ }
+
+ private void addContentView(View v, int index) {
+ if (mContent != null) {
+ mContent.addView(v, index);
+ } else {
+ addView(v, index);
+ }
+ }
+
public void setForceShowIcon(boolean forceShow) {
mPreserveIconSpacing = mForceShowIcon = forceShow;
}
@@ -270,7 +285,7 @@ public class ListMenuItemView extends LinearLayout
LayoutInflater inflater = getInflater();
mIconView = (ImageView) inflater.inflate(com.android.internal.R.layout.list_menu_item_icon,
this, false);
- addView(mIconView, 0);
+ addContentView(mIconView, 0);
}
private void insertRadioButton() {
@@ -278,7 +293,7 @@ public class ListMenuItemView extends LinearLayout
mRadioButton =
(RadioButton) inflater.inflate(com.android.internal.R.layout.list_menu_item_radio,
this, false);
- addView(mRadioButton);
+ addContentView(mRadioButton);
}
private void insertCheckBox() {
@@ -286,7 +301,7 @@ public class ListMenuItemView extends LinearLayout
mCheckBox =
(CheckBox) inflater.inflate(com.android.internal.R.layout.list_menu_item_checkbox,
this, false);
- addView(mCheckBox);
+ addContentView(mCheckBox);
}
public boolean prefersCondensedTitle() {
diff --git a/com/android/internal/widget/ButtonBarLayout.java b/com/android/internal/widget/ButtonBarLayout.java
index 6a0edef4..ab8be335 100644
--- a/com/android/internal/widget/ButtonBarLayout.java
+++ b/com/android/internal/widget/ButtonBarLayout.java
@@ -30,9 +30,6 @@ import com.android.internal.R;
* orientation when it can't fit its child views horizontally.
*/
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;
@@ -46,12 +43,8 @@ public class ButtonBarLayout extends LinearLayout {
public ButtonBarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
- final boolean allowStackingDefault =
- context.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();
}
@@ -151,7 +144,7 @@ public class ButtonBarLayout extends LinearLayout {
private void setStacked(boolean stacked) {
setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
- setGravity(stacked ? Gravity.RIGHT : Gravity.BOTTOM);
+ setGravity(stacked ? Gravity.END : Gravity.BOTTOM);
final View spacer = findViewById(R.id.spacer);
if (spacer != null) {
diff --git a/com/android/internal/widget/ExploreByTouchHelper.java b/com/android/internal/widget/ExploreByTouchHelper.java
index 759a41a2..50ad547e 100644
--- a/com/android/internal/widget/ExploreByTouchHelper.java
+++ b/com/android/internal/widget/ExploreByTouchHelper.java
@@ -186,9 +186,6 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {
}
final AccessibilityEvent event = createEvent(virtualViewId, eventType);
- if (event == null) {
- return false;
- }
return parent.requestSendAccessibilityEvent(mView, event);
}
@@ -243,9 +240,6 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {
if (parent != null) {
final AccessibilityEvent event = createEvent(virtualViewId,
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- if (event == null) {
- return;
- }
event.setContentChangeTypes(changeTypes);
parent.requestSendAccessibilityEvent(mView, event);
}
@@ -311,9 +305,6 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {
* the specified item.
*/
private AccessibilityEvent createEventForHost(int eventType) {
- if (!AccessibilityManager.getInstance(mContext).isObservedEventType(eventType)) {
- return null;
- }
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
mView.onInitializeAccessibilityEvent(event);
@@ -334,9 +325,6 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {
* the specified item.
*/
private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) {
- if (!AccessibilityManager.getInstance(mContext).isObservedEventType(eventType)) {
- return null;
- }
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.setEnabled(true);
event.setClassName(DEFAULT_CLASS_NAME);
diff --git a/com/android/internal/widget/LockPatternUtils.java b/com/android/internal/widget/LockPatternUtils.java
index 4be6b28a..e871003a 100644
--- a/com/android/internal/widget/LockPatternUtils.java
+++ b/com/android/internal/widget/LockPatternUtils.java
@@ -27,7 +27,6 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.UserInfo;
import android.os.AsyncTask;
-import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -35,7 +34,6 @@ import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
-import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.IStorageManager;
@@ -964,9 +962,12 @@ public class LockPatternUtils {
/**
* Retrieves whether the current profile and device locks can be unified.
+ * @param userHandle profile user handle.
*/
public boolean isSeparateProfileChallengeAllowedToUnify(int userHandle) {
- return getDevicePolicyManager().isProfileActivePasswordSufficientForParent(userHandle);
+ return getDevicePolicyManager().isProfileActivePasswordSufficientForParent(userHandle)
+ && !getUserManager().hasUserRestriction(
+ UserManager.DISALLOW_UNIFIED_PASSWORD, UserHandle.of(userHandle));
}
private boolean hasSeparateChallenge(int userHandle) {
diff --git a/com/android/internal/widget/NotificationActionListLayout.java b/com/android/internal/widget/NotificationActionListLayout.java
index 26023b49..e013553e 100644
--- a/com/android/internal/widget/NotificationActionListLayout.java
+++ b/com/android/internal/widget/NotificationActionListLayout.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RippleDrawable;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.Gravity;
@@ -203,6 +204,11 @@ public class NotificationActionListLayout extends LinearLayout {
public void onViewAdded(View child) {
super.onViewAdded(child);
clearMeasureOrder();
+ // For some reason ripples + notification actions seem to be an unhappy combination
+ // b/69474443 so just turn them off for now.
+ if (child.getBackground() instanceof RippleDrawable) {
+ ((RippleDrawable)child.getBackground()).setForceSoftware(true);
+ }
}
@Override
diff --git a/com/android/internal/widget/RecyclerView.java b/com/android/internal/widget/RecyclerView.java
index 408a4e9b..7abc76a8 100644
--- a/com/android/internal/widget/RecyclerView.java
+++ b/com/android/internal/widget/RecyclerView.java
@@ -9556,7 +9556,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;
}
diff --git a/com/android/internal/widget/ResolverDrawerLayout.java b/com/android/internal/widget/ResolverDrawerLayout.java
index 17c7ebd3..7635a727 100644
--- a/com/android/internal/widget/ResolverDrawerLayout.java
+++ b/com/android/internal/widget/ResolverDrawerLayout.java
@@ -81,6 +81,7 @@ public class ResolverDrawerLayout extends ViewGroup {
private int mCollapsibleHeightReserved;
private int mTopOffset;
+ private boolean mShowAtTop;
private boolean mIsDragging;
private boolean mOpenOnClick;
@@ -134,6 +135,7 @@ public class ResolverDrawerLayout extends ViewGroup {
mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
mMaxCollapsedHeight);
+ mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
a.recycle();
mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material);
@@ -162,6 +164,16 @@ public class ResolverDrawerLayout extends ViewGroup {
return mCollapseOffset > 0;
}
+ public void setShowAtTop(boolean showOnTop) {
+ mShowAtTop = showOnTop;
+ invalidate();
+ requestLayout();
+ }
+
+ public boolean getShowAtTop() {
+ return mShowAtTop;
+ }
+
public void setCollapsed(boolean collapsed) {
if (!isLaidOut()) {
mOpenOnLayout = collapsed;
@@ -206,6 +218,12 @@ public class ResolverDrawerLayout extends ViewGroup {
return false;
}
+ if (getShowAtTop()) {
+ // Keep the drawer fully open.
+ mCollapseOffset = 0;
+ return false;
+ }
+
if (isLaidOut()) {
final boolean isCollapsedOld = mCollapseOffset != 0;
if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
@@ -372,14 +390,23 @@ public class ResolverDrawerLayout extends ViewGroup {
mVelocityTracker.computeCurrentVelocity(1000);
final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
if (Math.abs(yvel) > mMinFlingVelocity) {
- if (isDismissable()
- && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
- smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel);
- mDismissOnScrollerFinished = true;
+ if (getShowAtTop()) {
+ if (isDismissable() && yvel < 0) {
+ abortAnimation();
+ dismiss();
+ } else {
+ smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+ }
} else {
- smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+ if (isDismissable()
+ && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel);
+ mDismissOnScrollerFinished = true;
+ } else {
+ smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+ }
}
- } else {
+ }else {
smoothScrollTo(
mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
}
@@ -421,6 +448,11 @@ public class ResolverDrawerLayout extends ViewGroup {
mVelocityTracker.clear();
}
+ private void dismiss() {
+ mRunOnDismissedListener = new RunOnDismissedListener();
+ post(mRunOnDismissedListener);
+ }
+
@Override
public void computeScroll() {
super.computeScroll();
@@ -430,8 +462,7 @@ public class ResolverDrawerLayout extends ViewGroup {
if (keepGoing) {
postInvalidateOnAnimation();
} else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
- mRunOnDismissedListener = new RunOnDismissedListener();
- post(mRunOnDismissedListener);
+ dismiss();
}
}
}
@@ -443,6 +474,10 @@ public class ResolverDrawerLayout extends ViewGroup {
}
private float performDrag(float dy) {
+ if (getShowAtTop()) {
+ return 0;
+ }
+
final float newPos = Math.max(0, Math.min(mCollapseOffset + dy,
mCollapsibleHeight + mUncollapsibleHeight));
if (newPos != mCollapseOffset) {
@@ -656,7 +691,7 @@ public class ResolverDrawerLayout extends ViewGroup {
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
- if (velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
+ if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
smoothScrollTo(0, velocityY);
return true;
}
@@ -666,12 +701,21 @@ public class ResolverDrawerLayout extends ViewGroup {
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
- if (isDismissable()
- && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
- smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
- mDismissOnScrollerFinished = true;
+ if (getShowAtTop()) {
+ if (isDismissable() && velocityY > 0) {
+ abortAnimation();
+ dismiss();
+ } else {
+ smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
+ }
} else {
- smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
+ if (isDismissable()
+ && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
+ mDismissOnScrollerFinished = true;
+ } else {
+ smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
+ }
}
return true;
}
@@ -794,7 +838,11 @@ public class ResolverDrawerLayout extends ViewGroup {
updateCollapseOffset(oldCollapsibleHeight, !isDragging());
- mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
+ if (getShowAtTop()) {
+ mTopOffset = 0;
+ } else {
+ mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
+ }
setMeasuredDimension(sourceWidth, heightSize);
}
diff --git a/com/android/keyguard/KeyguardAbsKeyInputView.java b/com/android/keyguard/KeyguardAbsKeyInputView.java
index 775b9e8a..a9804139 100644
--- a/com/android/keyguard/KeyguardAbsKeyInputView.java
+++ b/com/android/keyguard/KeyguardAbsKeyInputView.java
@@ -16,8 +16,8 @@
package com.android.keyguard;
-import static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL;
-import static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED;
+import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL;
+import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED;
import android.content.Context;
import android.os.AsyncTask;
@@ -29,6 +29,7 @@ import android.view.KeyEvent;
import android.view.View;
import android.widget.LinearLayout;
+import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternChecker;
import com.android.internal.widget.LockPatternUtils;
diff --git a/com/android/keyguard/KeyguardPatternView.java b/com/android/keyguard/KeyguardPatternView.java
index ec5f356a..d636316d 100644
--- a/com/android/keyguard/KeyguardPatternView.java
+++ b/com/android/keyguard/KeyguardPatternView.java
@@ -15,8 +15,8 @@
*/
package com.android.keyguard;
-import static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL;
-import static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED;
+import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL;
+import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED;
import android.content.Context;
import android.graphics.Rect;
@@ -33,6 +33,7 @@ import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.LinearLayout;
+import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternChecker;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockPatternView;
diff --git a/com/android/keyguard/KeyguardSecurityContainer.java b/com/android/keyguard/KeyguardSecurityContainer.java
index 27bc599f..9f393215 100644
--- a/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/com/android/keyguard/KeyguardSecurityContainer.java
@@ -49,6 +49,7 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe
private boolean mIsVerifyUnlockOnly;
private SecurityMode mCurrentSecuritySelection = SecurityMode.Invalid;
private SecurityCallback mSecurityCallback;
+ private AlertDialog mAlertDialog;
private final KeyguardUpdateMonitor mUpdateMonitor;
@@ -95,6 +96,10 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe
@Override
public void onPause() {
+ if (mAlertDialog != null) {
+ mAlertDialog.dismiss();
+ mAlertDialog = null;
+ }
if (mCurrentSecuritySelection != SecurityMode.None) {
getSecurityView(mCurrentSecuritySelection).onPause();
}
@@ -174,16 +179,20 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe
}
private void showDialog(String title, String message) {
- final AlertDialog dialog = new AlertDialog.Builder(mContext)
+ if (mAlertDialog != null) {
+ mAlertDialog.dismiss();
+ }
+
+ mAlertDialog = new AlertDialog.Builder(mContext)
.setTitle(title)
.setMessage(message)
.setCancelable(false)
.setNeutralButton(R.string.ok, null)
.create();
if (!(mContext instanceof Activity)) {
- dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+ mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
}
- dialog.show();
+ mAlertDialog.show();
}
private void showTimeoutDialog(int userId, int timeoutMs) {
diff --git a/com/android/keyguard/KeyguardSimPinView.java b/com/android/keyguard/KeyguardSimPinView.java
index 432b4061..6e0b56e2 100644
--- a/com/android/keyguard/KeyguardSimPinView.java
+++ b/com/android/keyguard/KeyguardSimPinView.java
@@ -53,8 +53,13 @@ public class KeyguardSimPinView extends KeyguardPinBasedInputView {
private ProgressDialog mSimUnlockProgressDialog = null;
private CheckSimPin mCheckSimPinThread;
+ // Below flag is set to true during power-up or when a new SIM card inserted on device.
+ // When this is true and when SIM card is PIN locked state, on PIN lock screen, message would
+ // be displayed to inform user about the number of remaining PIN attempts left.
+ private boolean mShowDefaultMessage = true;
+ private int mRemainingAttempts = -1;
private AlertDialog mRemainingAttemptsDialog;
- private int mSubId;
+ private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private ImageView mSimImageView;
KeyguardUpdateMonitorCallback mUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() {
@@ -91,32 +96,69 @@ public class KeyguardSimPinView extends KeyguardPinBasedInputView {
public void resetState() {
super.resetState();
if (DEBUG) Log.v(TAG, "Resetting state");
- KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext);
- mSubId = monitor.getNextSubIdForState(IccCardConstants.State.PIN_REQUIRED);
+ handleSubInfoChangeIfNeeded();
+ if (mShowDefaultMessage) {
+ showDefaultMessage();
+ }
boolean isEsimLocked = KeyguardEsimArea.isEsimLocked(mContext, mSubId);
- if (SubscriptionManager.isValidSubscriptionId(mSubId)) {
- int count = TelephonyManager.getDefault().getSimCount();
- Resources rez = getResources();
- String msg;
- int color = Color.WHITE;
- if (count < 2) {
- msg = rez.getString(R.string.kg_sim_pin_instructions);
- } else {
- SubscriptionInfo info = monitor.getSubscriptionInfoForSubId(mSubId);
- CharSequence displayName = info != null ? info.getDisplayName() : ""; // don't crash
- msg = rez.getString(R.string.kg_sim_pin_instructions_multi, displayName);
- if (info != null) {
- color = info.getIconTint();
- }
+
+ KeyguardEsimArea esimButton = findViewById(R.id.keyguard_esim_area);
+ esimButton.setVisibility(isEsimLocked ? View.VISIBLE : View.GONE);
+ }
+
+ private void showDefaultMessage() {
+ if (mRemainingAttempts >= 0) {
+ mSecurityMessageDisplay.setMessage(getPinPasswordErrorMessage(
+ mRemainingAttempts, true));
+ return;
+ }
+
+ boolean isEsimLocked = KeyguardEsimArea.isEsimLocked(mContext, mSubId);
+ int count = TelephonyManager.getDefault().getSimCount();
+ Resources rez = getResources();
+ String msg;
+ int color = Color.WHITE;
+ if (count < 2) {
+ msg = rez.getString(R.string.kg_sim_pin_instructions);
+ } else {
+ SubscriptionInfo info = KeyguardUpdateMonitor.getInstance(mContext).
+ getSubscriptionInfoForSubId(mSubId);
+ CharSequence displayName = info != null ? info.getDisplayName() : ""; // don't crash
+ msg = rez.getString(R.string.kg_sim_pin_instructions_multi, displayName);
+ if (info != null) {
+ color = info.getIconTint();
}
- if (isEsimLocked) {
- msg = msg + " " + rez.getString(R.string.kg_sim_lock_instructions_esim);
+ }
+
+ if (isEsimLocked) {
+ msg = msg + " " + rez.getString(R.string.kg_sim_lock_instructions_esim);
+ }
+
+ mSecurityMessageDisplay.setMessage(msg);
+ mSimImageView.setImageTintList(ColorStateList.valueOf(color));
+
+ // Sending empty PIN here to query the number of remaining PIN attempts
+ new CheckSimPin("", mSubId) {
+ void onSimCheckResponse(final int result, final int attemptsRemaining) {
+ Log.d(LOG_TAG, "onSimCheckResponse " + " dummy One result" + result +
+ " attemptsRemaining=" + attemptsRemaining);
+ if (attemptsRemaining >= 0) {
+ mRemainingAttempts = attemptsRemaining;
+ mSecurityMessageDisplay.setMessage(
+ getPinPasswordErrorMessage(attemptsRemaining, true));
+ }
}
- mSecurityMessageDisplay.setMessage(msg);
- mSimImageView.setImageTintList(ColorStateList.valueOf(color));
+ }.start();
+ }
+
+ private void handleSubInfoChangeIfNeeded() {
+ KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext);
+ int subId = monitor.getNextSubIdForState(IccCardConstants.State.PIN_REQUIRED);
+ if (subId != mSubId && SubscriptionManager.isValidSubscriptionId(subId)) {
+ mSubId = subId;
+ mShowDefaultMessage = true;
+ mRemainingAttempts = -1;
}
- KeyguardEsimArea esimButton = findViewById(R.id.keyguard_esim_area);
- esimButton.setVisibility(isEsimLocked ? View.VISIBLE : View.GONE);
}
@Override
@@ -131,17 +173,19 @@ public class KeyguardSimPinView extends KeyguardPinBasedInputView {
return 0;
}
- private String getPinPasswordErrorMessage(int attemptsRemaining) {
+ private String getPinPasswordErrorMessage(int attemptsRemaining, boolean isDefault) {
String displayMessage;
-
+ int msgId;
if (attemptsRemaining == 0) {
displayMessage = getContext().getString(R.string.kg_password_wrong_pin_code_pukked);
} else if (attemptsRemaining > 0) {
+ msgId = isDefault ? R.plurals.kg_password_default_pin_message :
+ R.plurals.kg_password_wrong_pin_code;
displayMessage = getContext().getResources()
- .getQuantityString(R.plurals.kg_password_wrong_pin_code, attemptsRemaining,
- attemptsRemaining);
+ .getQuantityString(msgId, attemptsRemaining, attemptsRemaining);
} else {
- displayMessage = getContext().getString(R.string.kg_password_pin_failed);
+ msgId = isDefault ? R.string.kg_sim_pin_instructions : R.string.kg_password_pin_failed;
+ displayMessage = getContext().getString(msgId);
}
if (DEBUG) Log.d(LOG_TAG, "getPinPasswordErrorMessage:"
+ " attemptsRemaining=" + attemptsRemaining + " displayMessage=" + displayMessage);
@@ -252,7 +296,7 @@ public class KeyguardSimPinView extends KeyguardPinBasedInputView {
}
private Dialog getSimRemainingAttemptsDialog(int remaining) {
- String msg = getPinPasswordErrorMessage(remaining);
+ String msg = getPinPasswordErrorMessage(remaining, false);
if (mRemainingAttemptsDialog == null) {
Builder builder = new AlertDialog.Builder(mContext);
builder.setMessage(msg);
@@ -288,6 +332,7 @@ public class KeyguardSimPinView extends KeyguardPinBasedInputView {
post(new Runnable() {
@Override
public void run() {
+ mRemainingAttempts = attemptsRemaining;
if (mSimUnlockProgressDialog != null) {
mSimUnlockProgressDialog.hide();
}
@@ -296,8 +341,13 @@ public class KeyguardSimPinView extends KeyguardPinBasedInputView {
if (result == PhoneConstants.PIN_RESULT_SUCCESS) {
KeyguardUpdateMonitor.getInstance(getContext())
.reportSimUnlocked(mSubId);
- mCallback.dismiss(true, KeyguardUpdateMonitor.getCurrentUser());
+ mRemainingAttempts = -1;
+ mShowDefaultMessage = true;
+ if (mCallback != null) {
+ mCallback.dismiss(true, KeyguardUpdateMonitor.getCurrentUser());
+ }
} else {
+ mShowDefaultMessage = false;
if (result == PhoneConstants.PIN_PASSWORD_INCORRECT) {
if (attemptsRemaining <= 2) {
// this is getting critical - show dialog
@@ -305,7 +355,7 @@ public class KeyguardSimPinView extends KeyguardPinBasedInputView {
} else {
// show message
mSecurityMessageDisplay.setMessage(
- getPinPasswordErrorMessage(attemptsRemaining));
+ getPinPasswordErrorMessage(attemptsRemaining, false));
}
} else {
// "PIN operation failed!" - no idea what this was and no way to
diff --git a/com/android/keyguard/KeyguardSimPukView.java b/com/android/keyguard/KeyguardSimPukView.java
index 7f79008b..876d170e 100644
--- a/com/android/keyguard/KeyguardSimPukView.java
+++ b/com/android/keyguard/KeyguardSimPukView.java
@@ -52,11 +52,17 @@ public class KeyguardSimPukView extends KeyguardPinBasedInputView {
private ProgressDialog mSimUnlockProgressDialog = null;
private CheckSimPuk mCheckSimPukThread;
+
+ // Below flag is set to true during power-up or when a new SIM card inserted on device.
+ // When this is true and when SIM card is PUK locked state, on PIN lock screen, message would
+ // be displayed to inform user about the number of remaining PUK attempts left.
+ private boolean mShowDefaultMessage = true;
+ private int mRemainingAttempts = -1;
private String mPukText;
private String mPinText;
private StateMachine mStateMachine = new StateMachine();
private AlertDialog mRemainingAttemptsDialog;
- private int mSubId;
+ private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private ImageView mSimImageView;
KeyguardUpdateMonitorCallback mUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() {
@@ -132,34 +138,17 @@ public class KeyguardSimPukView extends KeyguardPinBasedInputView {
}
}
+
void reset() {
mPinText="";
mPukText="";
state = ENTER_PUK;
- KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext);
- mSubId = monitor.getNextSubIdForState(IccCardConstants.State.PUK_REQUIRED);
- boolean isEsimLocked = KeyguardEsimArea.isEsimLocked(mContext, mSubId);
- if (SubscriptionManager.isValidSubscriptionId(mSubId)) {
- int count = TelephonyManager.getDefault().getSimCount();
- Resources rez = getResources();
- String msg;
- int color = Color.WHITE;
- if (count < 2) {
- msg = rez.getString(R.string.kg_puk_enter_puk_hint);
- } else {
- SubscriptionInfo info = monitor.getSubscriptionInfoForSubId(mSubId);
- CharSequence displayName = info != null ? info.getDisplayName() : "";
- msg = rez.getString(R.string.kg_puk_enter_puk_hint_multi, displayName);
- if (info != null) {
- color = info.getIconTint();
- }
- }
- if (isEsimLocked) {
- msg = msg + " " + rez.getString(R.string.kg_sim_lock_instructions_esim);
- }
- mSecurityMessageDisplay.setMessage(msg);
- mSimImageView.setImageTintList(ColorStateList.valueOf(color));
+ handleSubInfoChangeIfNeeded();
+ if (mShowDefaultMessage) {
+ showDefaultMessage();
}
+ boolean isEsimLocked = KeyguardEsimArea.isEsimLocked(mContext, mSubId);
+
KeyguardEsimArea esimButton = findViewById(R.id.keyguard_esim_area);
esimButton.setVisibility(isEsimLocked ? View.VISIBLE : View.GONE);
mPasswordEntry.requestFocus();
@@ -168,23 +157,79 @@ public class KeyguardSimPukView extends KeyguardPinBasedInputView {
}
+ private void showDefaultMessage() {
+ if (mRemainingAttempts >= 0) {
+ mSecurityMessageDisplay.setMessage(getPukPasswordErrorMessage(
+ mRemainingAttempts, true));
+ return;
+ }
+
+ boolean isEsimLocked = KeyguardEsimArea.isEsimLocked(mContext, mSubId);
+ int count = TelephonyManager.getDefault().getSimCount();
+ Resources rez = getResources();
+ String msg;
+ int color = Color.WHITE;
+ if (count < 2) {
+ msg = rez.getString(R.string.kg_puk_enter_puk_hint);
+ } else {
+ SubscriptionInfo info = KeyguardUpdateMonitor.getInstance(mContext).
+ getSubscriptionInfoForSubId(mSubId);
+ CharSequence displayName = info != null ? info.getDisplayName() : "";
+ msg = rez.getString(R.string.kg_puk_enter_puk_hint_multi, displayName);
+ if (info != null) {
+ color = info.getIconTint();
+ }
+ }
+ if (isEsimLocked) {
+ msg = msg + " " + rez.getString(R.string.kg_sim_lock_instructions_esim);
+ }
+ mSecurityMessageDisplay.setMessage(msg);
+ mSimImageView.setImageTintList(ColorStateList.valueOf(color));
+
+ // Sending empty PUK here to query the number of remaining PIN attempts
+ new CheckSimPuk("", "", mSubId) {
+ void onSimLockChangedResponse(final int result, final int attemptsRemaining) {
+ Log.d(LOG_TAG, "onSimCheckResponse " + " dummy One result" + result +
+ " attemptsRemaining=" + attemptsRemaining);
+ if (attemptsRemaining >= 0) {
+ mRemainingAttempts = attemptsRemaining;
+ mSecurityMessageDisplay.setMessage(
+ getPukPasswordErrorMessage(attemptsRemaining, true));
+ }
+ }
+ }.start();
+ }
+
+ private void handleSubInfoChangeIfNeeded() {
+ KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext);
+ int subId = monitor.getNextSubIdForState(IccCardConstants.State.PUK_REQUIRED);
+ if (subId != mSubId && SubscriptionManager.isValidSubscriptionId(subId)) {
+ mSubId = subId;
+ mShowDefaultMessage = true;
+ mRemainingAttempts = -1;
+ }
+ }
+
@Override
protected int getPromtReasonStringRes(int reason) {
// No message on SIM Puk
return 0;
}
- private String getPukPasswordErrorMessage(int attemptsRemaining) {
+ private String getPukPasswordErrorMessage(int attemptsRemaining, boolean isDefault) {
String displayMessage;
if (attemptsRemaining == 0) {
displayMessage = getContext().getString(R.string.kg_password_wrong_puk_code_dead);
} else if (attemptsRemaining > 0) {
+ int msgId = isDefault ? R.plurals.kg_password_default_puk_message :
+ R.plurals.kg_password_wrong_puk_code;
displayMessage = getContext().getResources()
- .getQuantityString(R.plurals.kg_password_wrong_puk_code, attemptsRemaining,
- attemptsRemaining);
+ .getQuantityString(msgId, attemptsRemaining, attemptsRemaining);
} else {
- displayMessage = getContext().getString(R.string.kg_password_puk_failed);
+ int msgId = isDefault ? R.string.kg_puk_enter_puk_hint :
+ R.string.kg_password_puk_failed;
+ displayMessage = getContext().getString(msgId);
}
if (DEBUG) Log.d(LOG_TAG, "getPukPasswordErrorMessage:"
+ " attemptsRemaining=" + attemptsRemaining + " displayMessage=" + displayMessage);
@@ -303,7 +348,7 @@ public class KeyguardSimPukView extends KeyguardPinBasedInputView {
}
private Dialog getPukRemainingAttemptsDialog(int remaining) {
- String msg = getPukPasswordErrorMessage(remaining);
+ String msg = getPukPasswordErrorMessage(remaining, false);
if (mRemainingAttemptsDialog == null) {
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setMessage(msg);
@@ -359,16 +404,25 @@ public class KeyguardSimPukView extends KeyguardPinBasedInputView {
if (result == PhoneConstants.PIN_RESULT_SUCCESS) {
KeyguardUpdateMonitor.getInstance(getContext())
.reportSimUnlocked(mSubId);
- mCallback.dismiss(true, KeyguardUpdateMonitor.getCurrentUser());
+ mRemainingAttempts = -1;
+ mShowDefaultMessage = true;
+ if (mCallback != null) {
+ mCallback.dismiss(true, KeyguardUpdateMonitor.getCurrentUser());
+ }
} else {
+ mShowDefaultMessage = false;
if (result == PhoneConstants.PIN_PASSWORD_INCORRECT) {
+ // show message
+ mSecurityMessageDisplay.setMessage(getPukPasswordErrorMessage(
+ attemptsRemaining, false));
if (attemptsRemaining <= 2) {
// this is getting critical - show dialog
getPukRemainingAttemptsDialog(attemptsRemaining).show();
} else {
// show message
mSecurityMessageDisplay.setMessage(
- getPukPasswordErrorMessage(attemptsRemaining));
+ getPukPasswordErrorMessage(
+ attemptsRemaining, false));
}
} else {
mSecurityMessageDisplay.setMessage(getContext().getString(
diff --git a/com/android/keyguard/KeyguardSliceView.java b/com/android/keyguard/KeyguardSliceView.java
index cb3d59c4..b9bf80de 100644
--- a/com/android/keyguard/KeyguardSliceView.java
+++ b/com/android/keyguard/KeyguardSliceView.java
@@ -17,38 +17,56 @@
package com.android.keyguard;
import android.app.PendingIntent;
-import android.app.slice.Slice;
-import android.app.slice.SliceItem;
-import android.app.slice.SliceQuery;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.Observer;
import android.content.Context;
-import android.database.ContentObserver;
+import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.os.Handler;
+import android.provider.Settings;
import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.internal.graphics.ColorUtils;
+import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.keyguard.KeyguardSliceProvider;
+import com.android.systemui.tuner.TunerService;
-import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.Consumer;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceItem;
+import androidx.app.slice.core.SliceQuery;
+import androidx.app.slice.widget.SliceLiveData;
/**
* View visible under the clock on the lock screen and AoD.
*/
-public class KeyguardSliceView extends LinearLayout {
+public class KeyguardSliceView extends LinearLayout implements View.OnClickListener,
+ Observer<Slice>, TunerService.Tunable {
- private final Uri mKeyguardSliceUri;
+ private static final String TAG = "KeyguardSliceView";
+ private final HashMap<View, PendingIntent> mClickActions;
+ private Uri mKeyguardSliceUri;
private TextView mTitle;
- private TextView mText;
- private Slice mSlice;
- private PendingIntent mSliceAction;
+ private LinearLayout mRow;
private int mTextColor;
private float mDarkAmount = 0;
- private final ContentObserver mObserver;
+ private LiveData<Slice> mLiveData;
+ private int mIconSize;
+ private Consumer<Boolean> mListener;
+ private boolean mHasHeader;
public KeyguardSliceView(Context context) {
this(context, null, 0);
@@ -60,16 +78,20 @@ public class KeyguardSliceView extends LinearLayout {
public KeyguardSliceView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mObserver = new KeyguardSliceObserver(new Handler());
- mKeyguardSliceUri = Uri.parse(KeyguardSliceProvider.KEYGUARD_SLICE_URI);;
+
+ TunerService tunerService = Dependency.get(TunerService.class);
+ tunerService.addTunable(this, Settings.Secure.KEYGUARD_SLICE_URI);
+
+ mClickActions = new HashMap<>();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mTitle = findViewById(R.id.title);
- mText = findViewById(R.id.text);
- mTextColor = mTitle.getCurrentTextColor();
+ mRow = findViewById(R.id.row);
+ mTextColor = Utils.getColorAttr(mContext, R.attr.wallpaperTextColor);
+ mIconSize = (int) mContext.getResources().getDimension(R.dimen.widget_icon_size);
}
@Override
@@ -77,57 +99,103 @@ public class KeyguardSliceView extends LinearLayout {
super.onAttachedToWindow();
// Set initial content
- showSlice(Slice.bindSlice(getContext().getContentResolver(), mKeyguardSliceUri,
- Collections.emptyList()));
+ showSlice(Slice.bindSlice(getContext(), mKeyguardSliceUri));
// Make sure we always have the most current slice
- getContext().getContentResolver().registerContentObserver(mKeyguardSliceUri,
- false /* notifyDescendants */, mObserver);
+ mLiveData.observeForever(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- getContext().getContentResolver().unregisterContentObserver(mObserver);
+ mLiveData.removeObserver(this);
}
private void showSlice(Slice slice) {
- // Items will be wrapped into an action when they have tap targets.
- SliceItem actionSlice = SliceQuery.find(slice, SliceItem.FORMAT_ACTION);
- if (actionSlice != null) {
- mSlice = actionSlice.getSlice();
- mSliceAction = actionSlice.getAction();
- } else {
- mSlice = slice;
- mSliceAction = null;
- }
- if (mSlice == null) {
- setVisibility(GONE);
- return;
- }
+ // Main area
+ SliceItem mainItem = SliceQuery.find(slice, android.app.slice.SliceItem.FORMAT_SLICE,
+ null /* hints */, new String[]{android.app.slice.Slice.HINT_LIST_ITEM});
+ mHasHeader = mainItem != null;
- SliceItem title = SliceQuery.find(mSlice, SliceItem.FORMAT_TEXT, Slice.HINT_TITLE, null);
- if (title == null) {
+ List<SliceItem> subItems = SliceQuery.findAll(slice,
+ android.app.slice.SliceItem.FORMAT_SLICE,
+ new String[]{android.app.slice.Slice.HINT_LIST_ITEM},
+ null /* nonHints */);
+
+ if (!mHasHeader) {
mTitle.setVisibility(GONE);
} else {
mTitle.setVisibility(VISIBLE);
- mTitle.setText(title.getText());
+ SliceItem mainTitle = SliceQuery.find(mainItem.getSlice(),
+ android.app.slice.SliceItem.FORMAT_TEXT,
+ new String[]{android.app.slice.Slice.HINT_TITLE},
+ null /* nonHints */);
+ mTitle.setText(mainTitle.getText());
}
- SliceItem text = SliceQuery.find(mSlice, SliceItem.FORMAT_TEXT, null, Slice.HINT_TITLE);
- if (text == null) {
- mText.setVisibility(GONE);
- } else {
- mText.setVisibility(VISIBLE);
- mText.setText(text.getText());
+ mClickActions.clear();
+ final int subItemsCount = subItems.size();
+
+ for (int i = 0; i < subItemsCount; i++) {
+ SliceItem item = subItems.get(i);
+ final Uri itemTag = item.getSlice().getUri();
+ // Try to reuse the view if already exists in the layout
+ KeyguardSliceButton button = mRow.findViewWithTag(itemTag);
+ if (button == null) {
+ button = new KeyguardSliceButton(mContext);
+ button.setTextColor(mTextColor);
+ button.setTag(itemTag);
+ } else {
+ mRow.removeView(button);
+ }
+ button.setHasDivider(i < subItemsCount - 1);
+ mRow.addView(button, i);
+
+ PendingIntent pendingIntent;
+ try {
+ pendingIntent = item.getAction();
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Cannot retrieve action from keyguard slice", e);
+ pendingIntent = null;
+ }
+ mClickActions.put(button, pendingIntent);
+
+ SliceItem title = SliceQuery.find(item.getSlice(),
+ android.app.slice.SliceItem.FORMAT_TEXT,
+ new String[]{android.app.slice.Slice.HINT_TITLE},
+ null /* nonHints */);
+ button.setText(title.getText());
+
+ Drawable iconDrawable = null;
+ SliceItem icon = SliceQuery.find(item.getSlice(),
+ android.app.slice.SliceItem.FORMAT_IMAGE);
+ if (icon != null) {
+ iconDrawable = icon.getIcon().loadDrawable(mContext);
+ final int width = (int) (iconDrawable.getIntrinsicWidth()
+ / (float) iconDrawable.getIntrinsicHeight() * mIconSize);
+ iconDrawable.setBounds(0, 0, Math.max(width, 1), mIconSize);
+ }
+ button.setCompoundDrawablesRelative(iconDrawable, null, null, null);
+ button.setOnClickListener(this);
+ }
+
+ // Removing old views
+ for (int i = 0; i < mRow.getChildCount(); i++) {
+ View child = mRow.getChildAt(i);
+ if (!mClickActions.containsKey(child)) {
+ mRow.removeView(child);
+ i--;
+ }
}
- final int visibility = title == null && text == null ? GONE : VISIBLE;
+ final int visibility = mHasHeader || subItemsCount > 0 ? VISIBLE : GONE;
if (visibility != getVisibility()) {
setVisibility(visibility);
}
+
+ mListener.accept(mHasHeader);
}
public void setDark(float darkAmount) {
@@ -135,30 +203,113 @@ public class KeyguardSliceView extends LinearLayout {
updateTextColors();
}
- public void setTextColor(int textColor) {
- mTextColor = textColor;
- }
-
private void updateTextColors() {
final int blendedColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
mTitle.setTextColor(blendedColor);
- mText.setTextColor(blendedColor);
+ int childCount = mRow.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View v = mRow.getChildAt(i);
+ if (v instanceof Button) {
+ ((Button) v).setTextColor(blendedColor);
+ }
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ final PendingIntent action = mClickActions.get(v);
+ if (action != null) {
+ try {
+ action.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.i(TAG, "Pending intent cancelled, nothing to launch", e);
+ }
+ }
}
- private class KeyguardSliceObserver extends ContentObserver {
- KeyguardSliceObserver(Handler handler) {
- super(handler);
+ public void setListener(Consumer<Boolean> listener) {
+ mListener = listener;
+ }
+
+ public boolean hasHeader() {
+ return mHasHeader;
+ }
+
+ /**
+ * LiveData observer lifecycle.
+ * @param slice the new slice content.
+ */
+ @Override
+ public void onChanged(Slice slice) {
+ showSlice(slice);
+ }
+
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ setupUri(newValue);
+ }
+
+ public void setupUri(String uriString) {
+ if (uriString == null) {
+ uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI;
+ }
+
+ boolean wasObserving = false;
+ if (mLiveData != null && mLiveData.hasActiveObservers()) {
+ wasObserving = true;
+ mLiveData.removeObserver(this);
+ }
+
+ mKeyguardSliceUri = Uri.parse(uriString);
+ mLiveData = SliceLiveData.fromUri(mContext, mKeyguardSliceUri);
+
+ if (wasObserving) {
+ mLiveData.observeForever(this);
+ showSlice(Slice.bindSlice(getContext(), mKeyguardSliceUri));
+ }
+ }
+
+ /**
+ * Representation of an item that appears under the clock on main keyguard message.
+ * Shows optional separator.
+ */
+ private class KeyguardSliceButton extends Button {
+
+ private final Paint mPaint;
+ private boolean mHasDivider;
+
+ public KeyguardSliceButton(Context context) {
+ super(context, null /* attrs */,
+ com.android.keyguard.R.style.TextAppearance_Keyguard_Secondary);
+ mPaint = new Paint();
+ mPaint.setStyle(Paint.Style.STROKE);
+ float dividerWidth = context.getResources()
+ .getDimension(R.dimen.widget_separator_thickness);
+ mPaint.setStrokeWidth(dividerWidth);
+ int horizontalPadding = (int) context.getResources()
+ .getDimension(R.dimen.widget_horizontal_padding);
+ setPadding(horizontalPadding, 0, horizontalPadding, 0);
+ setCompoundDrawablePadding((int) context.getResources()
+ .getDimension(R.dimen.widget_icon_padding));
+ }
+
+ public void setHasDivider(boolean hasDivider) {
+ mHasDivider = hasDivider;
}
@Override
- public void onChange(boolean selfChange) {
- this.onChange(selfChange, null);
+ public void setTextColor(int color) {
+ super.setTextColor(color);
+ mPaint.setColor(color);
}
@Override
- public void onChange(boolean selfChange, Uri uri) {
- showSlice(Slice.bindSlice(getContext().getContentResolver(), mKeyguardSliceUri,
- Collections.emptyList()));
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mHasDivider) {
+ final int lineX = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : getWidth();
+ canvas.drawLine(lineX, 0, lineX, getHeight(), mPaint);
+ }
}
}
}
diff --git a/com/android/keyguard/KeyguardStatusView.java b/com/android/keyguard/KeyguardStatusView.java
index 78cf2b9b..4b9a8744 100644
--- a/com/android/keyguard/KeyguardStatusView.java
+++ b/com/android/keyguard/KeyguardStatusView.java
@@ -28,6 +28,7 @@ import android.os.UserHandle;
import android.support.v4.graphics.ColorUtils;
import android.text.TextUtils;
import android.text.format.DateFormat;
+import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Slog;
@@ -38,11 +39,11 @@ import android.widget.GridLayout;
import android.widget.TextClock;
import android.widget.TextView;
-import com.android.internal.util.ArrayUtils;
import com.android.internal.widget.LockPatternUtils;
-import com.android.settingslib.Utils;
import com.android.systemui.ChargingView;
+import com.google.android.collect.Sets;
+
import java.util.Locale;
public class KeyguardStatusView extends GridLayout {
@@ -52,8 +53,11 @@ public class KeyguardStatusView extends GridLayout {
private final LockPatternUtils mLockPatternUtils;
private final AlarmManager mAlarmManager;
+ private final float mSmallClockScale;
+ private final float mWidgetPadding;
private TextClock mClockView;
+ private View mClockSeparator;
private TextView mOwnerInfo;
private ViewGroup mClockContainer;
private ChargingView mBatteryDoze;
@@ -61,7 +65,7 @@ public class KeyguardStatusView extends GridLayout {
private Runnable mPendingMarqueeStart;
private Handler mHandler;
- private View[] mVisibleInDoze;
+ private ArraySet<View> mVisibleInDoze;
private boolean mPulsing;
private float mDarkAmount = 0;
private int mTextColor;
@@ -112,6 +116,9 @@ public class KeyguardStatusView extends GridLayout {
mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mLockPatternUtils = new LockPatternUtils(getContext());
mHandler = new Handler(Looper.myLooper());
+ mSmallClockScale = getResources().getDimension(R.dimen.widget_small_font_size)
+ / getResources().getDimension(R.dimen.widget_big_font_size);
+ mWidgetPadding = getResources().getDimension(R.dimen.widget_vertical_padding);
}
private void setEnableMarquee(boolean enabled) {
@@ -150,9 +157,14 @@ public class KeyguardStatusView extends GridLayout {
mOwnerInfo = findViewById(R.id.owner_info);
mBatteryDoze = findViewById(R.id.battery_doze);
mKeyguardSlice = findViewById(R.id.keyguard_status_area);
- mVisibleInDoze = new View[]{mBatteryDoze, mClockView, mKeyguardSlice};
+ mClockSeparator = findViewById(R.id.clock_separator);
+ mVisibleInDoze = Sets.newArraySet(mBatteryDoze, mClockView, mKeyguardSlice,
+ mClockSeparator);
mTextColor = mClockView.getCurrentTextColor();
+ mKeyguardSlice.setListener(this::onSliceContentChanged);
+ onSliceContentChanged(mKeyguardSlice.hasHeader());
+
boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive();
setEnableMarquee(shouldMarquee);
refresh();
@@ -163,6 +175,22 @@ public class KeyguardStatusView extends GridLayout {
mClockView.setElegantTextHeight(false);
}
+ private void onSliceContentChanged(boolean hasHeader) {
+ final float clockScale = hasHeader ? mSmallClockScale : 1;
+ float translation = (mClockView.getHeight() - (mClockView.getHeight() * clockScale)) / 2f;
+ if (hasHeader) {
+ translation -= mWidgetPadding;
+ }
+ mClockView.setTranslationY(translation);
+ mClockView.setScaleX(clockScale);
+ mClockView.setScaleY(clockScale);
+ final float batteryTranslation =
+ -(mClockView.getWidth() - (mClockView.getWidth() * clockScale)) / 2;
+ mBatteryDoze.setTranslationX(batteryTranslation);
+ mBatteryDoze.setTranslationY(translation);
+ mClockSeparator.setVisibility(hasHeader ? VISIBLE : GONE);
+ }
+
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
@@ -201,17 +229,6 @@ public class KeyguardStatusView extends GridLayout {
return mClockView.getTextSize();
}
- public static String formatNextAlarm(Context context, AlarmManager.AlarmClockInfo info) {
- if (info == null) {
- return "";
- }
- String skeleton = DateFormat.is24HourFormat(context, ActivityManager.getCurrentUser())
- ? "EHm"
- : "Ehma";
- String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
- return DateFormat.format(pattern, info.getTriggerTime()).toString();
- }
-
private void updateOwnerInfo() {
if (mOwnerInfo == null) return;
String ownerInfo = getOwnerInfo();
@@ -303,7 +320,7 @@ public class KeyguardStatusView extends GridLayout {
final int N = mClockContainer.getChildCount();
for (int i = 0; i < N; i++) {
View child = mClockContainer.getChildAt(i);
- if (ArrayUtils.contains(mVisibleInDoze, child)) {
+ if (mVisibleInDoze.contains(child)) {
continue;
}
child.setAlpha(dark ? 0 : 1);
@@ -312,10 +329,12 @@ public class KeyguardStatusView extends GridLayout {
mOwnerInfo.setAlpha(dark ? 0 : 1);
}
+ final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, darkAmount);
updateDozeVisibleViews();
mBatteryDoze.setDark(dark);
mKeyguardSlice.setDark(darkAmount);
- mClockView.setTextColor(ColorUtils.blendARGB(mTextColor, Color.WHITE, darkAmount));
+ mClockView.setTextColor(blendedTextColor);
+ mClockSeparator.setBackgroundColor(blendedTextColor);
}
public void setPulsing(boolean pulsing) {
diff --git a/com/android/keyguard/KeyguardUpdateMonitor.java b/com/android/keyguard/KeyguardUpdateMonitor.java
index 41b007af..2058f15b 100644
--- a/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -71,6 +71,7 @@ import android.util.Log;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IccCardConstants;
import com.android.internal.telephony.IccCardConstants.State;
import com.android.internal.telephony.PhoneConstants;
@@ -347,6 +348,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
private SparseBooleanArray mUserFaceUnlockRunning = new SparseBooleanArray();
private static int sCurrentUser;
+ private Runnable mUpdateFingerprintListeningState = this::updateFingerprintListeningState;
public synchronized static void setCurrentUser(int currentUser) {
sCurrentUser = currentUser;
@@ -1110,7 +1112,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
}
}
- private KeyguardUpdateMonitor(Context context) {
+ @VisibleForTesting
+ protected KeyguardUpdateMonitor(Context context) {
mContext = context;
mSubscriptionManager = SubscriptionManager.from(context);
mDeviceProvisioned = isDeviceProvisionedInSettingsDb();
@@ -1666,7 +1669,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
public void setSwitchingUser(boolean switching) {
mSwitchingUser = switching;
- updateFingerprintListeningState();
+ // Since this comes in on a binder thread, we need to post if first
+ mHandler.post(mUpdateFingerprintListeningState);
}
private void sendUpdates(KeyguardUpdateMonitorCallback callback) {
diff --git a/com/android/keyguard/PasswordTextView.java b/com/android/keyguard/PasswordTextView.java
index 0219db33..d3dded0e 100644
--- a/com/android/keyguard/PasswordTextView.java
+++ b/com/android/keyguard/PasswordTextView.java
@@ -31,6 +31,7 @@ import android.os.PowerManager;
import android.os.SystemClock;
import android.provider.Settings;
import android.text.InputType;
+import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
@@ -78,6 +79,8 @@ public class PasswordTextView extends View {
*/
private static final float OVERSHOOT_TIME_POSITION = 0.5f;
+ private static char DOT = '\u2022';
+
/**
* The raw text size, will be multiplied by the scaled density when drawn
*/
@@ -208,7 +211,7 @@ public class PasswordTextView extends View {
public void append(char c) {
int visibleChars = mTextChars.size();
- String textbefore = mText;
+ CharSequence textbefore = getTransformedText();
mText = mText + c;
int newLength = mText.length();
CharState charState;
@@ -245,7 +248,7 @@ public class PasswordTextView extends View {
public void deleteLastChar() {
int length = mText.length();
- String textbefore = mText;
+ CharSequence textbefore = getTransformedText();
if (length > 0) {
mText = mText.substring(0, length - 1);
CharState charState = mTextChars.get(length - 1);
@@ -259,6 +262,21 @@ public class PasswordTextView extends View {
return mText;
}
+ private CharSequence getTransformedText() {
+ int textLength = mTextChars.size();
+ StringBuilder stringBuilder = new StringBuilder(textLength);
+ for (int i = 0; i < textLength; i++) {
+ CharState charState = mTextChars.get(i);
+ // If the dot is disappearing, the character is disappearing entirely. Consider
+ // it gone.
+ if (charState.dotAnimator != null && !charState.dotAnimationIsGrowing) {
+ continue;
+ }
+ stringBuilder.append(charState.isCharVisibleForA11y() ? charState.whichChar : DOT);
+ }
+ return stringBuilder;
+ }
+
private CharState obtainCharState(char c) {
CharState charState;
if(mCharPool.isEmpty()) {
@@ -272,7 +290,7 @@ public class PasswordTextView extends View {
}
public void reset(boolean animated, boolean announce) {
- String textbefore = mText;
+ CharSequence textbefore = getTransformedText();
mText = "";
int length = mTextChars.size();
int middleIndex = (length - 1) / 2;
@@ -305,17 +323,20 @@ public class PasswordTextView extends View {
}
}
- void sendAccessibilityEventTypeViewTextChanged(String beforeText, int fromIndex,
+ void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex,
int removedCount, int addedCount) {
- if (AccessibilityManager.getInstance(mContext).isObservedEventType(
- AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)
- && (isFocused() || isSelected() && isShown())) {
+ if (AccessibilityManager.getInstance(mContext).isEnabled() &&
+ (isFocused() || isSelected() && isShown())) {
AccessibilityEvent event =
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
event.setFromIndex(fromIndex);
event.setRemovedCount(removedCount);
event.setAddedCount(addedCount);
event.setBeforeText(beforeText);
+ CharSequence transformedText = getTransformedText();
+ if (!TextUtils.isEmpty(transformedText)) {
+ event.getText().add(transformedText);
+ }
event.setPassword(true);
sendAccessibilityEventUnchecked(event);
}
@@ -333,8 +354,9 @@ public class PasswordTextView extends View {
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
- info.setClassName(PasswordTextView.class.getName());
+ info.setClassName(EditText.class.getName());
info.setPassword(true);
+ info.setText(getTransformedText());
info.setEditable(true);
@@ -421,7 +443,19 @@ public class PasswordTextView extends View {
= new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
+ boolean textVisibleBefore = isCharVisibleForA11y();
+ float beforeTextSizeFactor = currentTextSizeFactor;
currentTextSizeFactor = (float) animation.getAnimatedValue();
+ if (textVisibleBefore != isCharVisibleForA11y()) {
+ currentTextSizeFactor = beforeTextSizeFactor;
+ CharSequence beforeText = getTransformedText();
+ currentTextSizeFactor = (float) animation.getAnimatedValue();
+ int indexOfThisChar = mTextChars.indexOf(CharState.this);
+ if (indexOfThisChar >= 0) {
+ sendAccessibilityEventTypeViewTextChanged(
+ beforeText, indexOfThisChar, 1, 1);
+ }
+ }
invalidate();
}
};
@@ -674,5 +708,13 @@ public class PasswordTextView extends View {
}
return charWidth + mCharPadding * currentWidthFactor;
}
+
+ public boolean isCharVisibleForA11y() {
+ // The text has size 0 when it is first added, but we want to count it as visible if
+ // it will become visible presently. Count text as visible if an animator
+ // is configured to make it grow.
+ boolean textIsGrowing = textAnimator != null && textAnimationIsGrowing;
+ return (currentTextSizeFactor > 0) || textIsGrowing;
+ }
}
}
diff --git a/com/android/keyguard/ViewMediatorCallback.java b/com/android/keyguard/ViewMediatorCallback.java
index b194de43..eff84c6a 100644
--- a/com/android/keyguard/ViewMediatorCallback.java
+++ b/com/android/keyguard/ViewMediatorCallback.java
@@ -76,6 +76,12 @@ public interface ViewMediatorCallback {
void playTrustedSound();
/**
+ * When the bouncer is shown or hides
+ * @param shown
+ */
+ void onBouncerVisiblityChanged(boolean shown);
+
+ /**
* @return true if the screen is on
*/
boolean isScreenOn();
diff --git a/com/android/layoutlib/bridge/Bridge.java b/com/android/layoutlib/bridge/Bridge.java
index 5dca8e7f..0cfc1811 100644
--- a/com/android/layoutlib/bridge/Bridge.java
+++ b/com/android/layoutlib/bridge/Bridge.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008 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.
@@ -14,659 +14,62 @@
* limitations under the License.
*/
-package com.android.layoutlib.bridge;
-
-import com.android.ide.common.rendering.api.Capability;
-import com.android.ide.common.rendering.api.DrawableParams;
-import com.android.ide.common.rendering.api.Features;
-import com.android.ide.common.rendering.api.LayoutLog;
-import com.android.ide.common.rendering.api.RenderSession;
+package com.android.layoutlib.bridge;import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.Result;
import com.android.ide.common.rendering.api.Result.Status;
import com.android.ide.common.rendering.api.SessionParams;
-import com.android.layoutlib.bridge.android.RenderParamsFlags;
-import com.android.layoutlib.bridge.impl.RenderDrawable;
-import com.android.layoutlib.bridge.impl.RenderSessionImpl;
-import com.android.layoutlib.bridge.util.DynamicIdMap;
-import com.android.ninepatch.NinePatchChunk;
-import com.android.resources.ResourceType;
-import com.android.tools.layoutlib.create.MethodAdapter;
-import com.android.tools.layoutlib.create.OverrideMethod;
-import com.android.util.Pair;
-
-import android.annotation.NonNull;
-import android.content.res.BridgeAssetManager;
-import android.graphics.Bitmap;
-import android.graphics.FontFamily_Delegate;
-import android.graphics.Typeface;
-import android.graphics.Typeface_Delegate;
-import android.icu.util.ULocale;
-import android.os.Looper;
-import android.os.Looper_Accessor;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-
-import java.io.File;
-import java.lang.ref.SoftReference;
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-import java.util.Arrays;
-import java.util.EnumMap;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.WeakHashMap;
-import java.util.concurrent.locks.ReentrantLock;
-import libcore.io.MemoryMappedFile_Delegate;
-
-import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
/**
- * Main entry point of the LayoutLib Bridge.
- * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call
- * {@link #createSession(SessionParams)}
+ * Legacy Bridge used in the SDK version of layoutlib
*/
public final class Bridge extends com.android.ide.common.rendering.api.Bridge {
+ private static final String SDK_NOT_SUPPORTED = "The SDK layoutlib version is not supported";
+ private static final Result NOT_SUPPORTED_RESULT =
+ Status.NOT_IMPLEMENTED.createResult(SDK_NOT_SUPPORTED);
+ private static BufferedImage sImage;
- private static final String ICU_LOCALE_DIRECTION_RTL = "right-to-left";
+ private static class BridgeRenderSession extends RenderSession {
- public static class StaticMethodNotImplementedException extends RuntimeException {
- private static final long serialVersionUID = 1L;
+ @Override
+ public synchronized BufferedImage getImage() {
+ if (sImage == null) {
+ sImage = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g = sImage.createGraphics();
+ g.clearRect(0, 0, 500, 500);
+ g.drawString(SDK_NOT_SUPPORTED, 20, 20);
+ g.dispose();
+ }
- public StaticMethodNotImplementedException(String msg) {
- super(msg);
+ return sImage;
}
- }
-
- /**
- * Lock to ensure only one rendering/inflating happens at a time.
- * This is due to some singleton in the Android framework.
- */
- private final static ReentrantLock sLock = new ReentrantLock();
-
- /**
- * Maps from id to resource type/name. This is for com.android.internal.R
- */
- @SuppressWarnings("deprecation")
- private final static Map<Integer, Pair<ResourceType, String>> sRMap = new HashMap<>();
- /**
- * Reverse map compared to sRMap, resource type -> (resource name -> id).
- * This is for com.android.internal.R.
- */
- private final static Map<ResourceType, Map<String, Integer>> sRevRMap = new EnumMap<>(ResourceType.class);
-
- // framework resources are defined as 0x01XX#### where XX is the resource type (layout,
- // drawable, etc...). Using FF as the type allows for 255 resource types before we get a
- // collision which should be fine.
- private final static int DYNAMIC_ID_SEED_START = 0x01ff0000;
- private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START);
-
- private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache =
- new WeakHashMap<>();
- private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache =
- new WeakHashMap<>();
-
- private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache = new HashMap<>();
- private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache =
- new HashMap<>();
-
- private static Map<String, Map<String, Integer>> sEnumValueMap;
- private static Map<String, String> sPlatformProperties;
-
- /**
- * A default log than prints to stdout/stderr.
- */
- private final static LayoutLog sDefaultLog = new LayoutLog() {
@Override
- public void error(String tag, String message, Object data) {
- System.err.println(message);
+ public Result render(long timeout, boolean forceMeasure) {
+ return NOT_SUPPORTED_RESULT;
}
@Override
- public void error(String tag, String message, Throwable throwable, Object data) {
- System.err.println(message);
+ public Result measure(long timeout) {
+ return NOT_SUPPORTED_RESULT;
}
@Override
- public void warning(String tag, String message, Object data) {
- System.out.println(message);
- }
- };
-
- /**
- * Current log.
- */
- private static LayoutLog sCurrentLog = sDefaultLog;
-
- private static final int LAST_SUPPORTED_FEATURE = Features.THEME_PREVIEW_NAVIGATION_BAR;
-
- @Override
- public int getApiLevel() {
- return com.android.ide.common.rendering.api.Bridge.API_CURRENT;
- }
-
- @SuppressWarnings("deprecation")
- @Override
- @Deprecated
- public EnumSet<Capability> getCapabilities() {
- // The Capability class is deprecated and frozen. All Capabilities enumerated there are
- // supported by this version of LayoutLibrary. So, it's safe to use EnumSet.allOf()
- return EnumSet.allOf(Capability.class);
- }
-
- @Override
- public boolean supports(int feature) {
- return feature <= LAST_SUPPORTED_FEATURE;
- }
-
- @Override
- public boolean init(Map<String,String> platformProperties,
- File fontLocation,
- Map<String, Map<String, Integer>> enumValueMap,
- LayoutLog log) {
- sPlatformProperties = platformProperties;
- sEnumValueMap = enumValueMap;
-
- BridgeAssetManager.initSystem();
-
- // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener
- // on static (native) methods which prints the signature on the console and
- // throws an exception.
- // This is useful when testing the rendering in ADT to identify static native
- // methods that are ignored -- layoutlib_create makes them returns 0/false/null
- // which is generally OK yet might be a problem, so this is how you'd find out.
- //
- // Currently layoutlib_create only overrides static native method.
- // Static non-natives are not overridden and thus do not get here.
- final String debug = System.getenv("DEBUG_LAYOUT");
- if (debug != null && !debug.equals("0") && !debug.equals("false")) {
-
- OverrideMethod.setDefaultListener(new MethodAdapter() {
- @Override
- public void onInvokeV(String signature, boolean isNative, Object caller) {
- sDefaultLog.error(null, "Missing Stub: " + signature +
- (isNative ? " (native)" : ""), null /*data*/);
-
- if (debug.equalsIgnoreCase("throw")) {
- // Throwing this exception doesn't seem that useful. It breaks
- // the layout editor yet doesn't display anything meaningful to the
- // user. Having the error in the console is just as useful. We'll
- // throw it only if the environment variable is "throw" or "THROW".
- throw new StaticMethodNotImplementedException(signature);
- }
- }
- });
- }
-
- // load the fonts.
- FontFamily_Delegate.setFontLocation(fontLocation.getAbsolutePath());
- MemoryMappedFile_Delegate.setDataDir(fontLocation.getAbsoluteFile().getParentFile());
-
- // now parse com.android.internal.R (and only this one as android.R is a subset of
- // the internal version), and put the content in the maps.
- try {
- Class<?> r = com.android.internal.R.class;
- // Parse the styleable class first, since it may contribute to attr values.
- parseStyleable();
-
- for (Class<?> inner : r.getDeclaredClasses()) {
- if (inner == com.android.internal.R.styleable.class) {
- // Already handled the styleable case. Not skipping attr, as there may be attrs
- // that are not referenced from styleables.
- continue;
- }
- String resTypeName = inner.getSimpleName();
- ResourceType resType = ResourceType.getEnum(resTypeName);
- if (resType != null) {
- Map<String, Integer> fullMap = null;
- switch (resType) {
- case ATTR:
- fullMap = sRevRMap.get(ResourceType.ATTR);
- break;
- case STRING:
- case STYLE:
- // Slightly less than thousand entries in each.
- fullMap = new HashMap<>(1280);
- // no break.
- default:
- if (fullMap == null) {
- fullMap = new HashMap<>();
- }
- sRevRMap.put(resType, fullMap);
- }
-
- for (Field f : inner.getDeclaredFields()) {
- // only process static final fields. Since the final attribute may have
- // been altered by layoutlib_create, we only check static
- if (!isValidRField(f)) {
- continue;
- }
- Class<?> type = f.getType();
- if (!type.isArray()) {
- Integer value = (Integer) f.get(null);
- //noinspection deprecation
- sRMap.put(value, Pair.of(resType, f.getName()));
- fullMap.put(f.getName(), value);
- }
- }
- }
- }
- } catch (Exception throwable) {
- if (log != null) {
- log.error(LayoutLog.TAG_BROKEN,
- "Failed to load com.android.internal.R from the layout library jar",
- throwable, null);
- }
- return false;
+ public Result getResult() {
+ return NOT_SUPPORTED_RESULT;
}
-
- return true;
- }
-
- /**
- * Tests if the field is pubic, static and one of int or int[].
- */
- private static boolean isValidRField(Field field) {
- int modifiers = field.getModifiers();
- boolean isAcceptable = Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers);
- Class<?> type = field.getType();
- return isAcceptable && type == int.class ||
- (type.isArray() && type.getComponentType() == int.class);
-
}
- private static void parseStyleable() throws Exception {
- // R.attr doesn't contain all the needed values. There are too many resources in the
- // framework for all to be in the R class. Only the ones specified manually in
- // res/values/symbols.xml are put in R class. Since, we need to create a map of all attr
- // values, we try and find them from the styleables.
-
- // There were 1500 elements in this map at M timeframe.
- Map<String, Integer> revRAttrMap = new HashMap<>(2048);
- sRevRMap.put(ResourceType.ATTR, revRAttrMap);
- // There were 2000 elements in this map at M timeframe.
- Map<String, Integer> revRStyleableMap = new HashMap<>(3072);
- sRevRMap.put(ResourceType.STYLEABLE, revRStyleableMap);
- Class<?> c = com.android.internal.R.styleable.class;
- Field[] fields = c.getDeclaredFields();
- // Sort the fields to bring all arrays to the beginning, so that indices into the array are
- // able to refer back to the arrays (i.e. no forward references).
- Arrays.sort(fields, (o1, o2) -> {
- if (o1 == o2) {
- return 0;
- }
- Class<?> t1 = o1.getType();
- Class<?> t2 = o2.getType();
- if (t1.isArray() && !t2.isArray()) {
- return -1;
- } else if (t2.isArray() && !t1.isArray()) {
- return 1;
- }
- return o1.getName().compareTo(o2.getName());
- });
- Map<String, int[]> styleables = new HashMap<>();
- for (Field field : fields) {
- if (!isValidRField(field)) {
- // Only consider public static fields that are int or int[].
- // Don't check the final flag as it may have been modified by layoutlib_create.
- continue;
- }
- String name = field.getName();
- if (field.getType().isArray()) {
- int[] styleableValue = (int[]) field.get(null);
- styleables.put(name, styleableValue);
- continue;
- }
- // Not an array.
- String arrayName = name;
- int[] arrayValue = null;
- int index;
- while ((index = arrayName.lastIndexOf('_')) >= 0) {
- // Find the name of the corresponding styleable.
- // Search in reverse order so that attrs like LinearLayout_Layout_layout_gravity
- // are mapped to LinearLayout_Layout and not to LinearLayout.
- arrayName = arrayName.substring(0, index);
- arrayValue = styleables.get(arrayName);
- if (arrayValue != null) {
- break;
- }
- }
- index = (Integer) field.get(null);
- if (arrayValue != null) {
- String attrName = name.substring(arrayName.length() + 1);
- int attrValue = arrayValue[index];
- //noinspection deprecation
- sRMap.put(attrValue, Pair.of(ResourceType.ATTR, attrName));
- revRAttrMap.put(attrName, attrValue);
- }
- //noinspection deprecation
- sRMap.put(index, Pair.of(ResourceType.STYLEABLE, name));
- revRStyleableMap.put(name, index);
- }
- }
@Override
- public boolean dispose() {
- BridgeAssetManager.clearSystem();
-
- // dispose of the default typeface.
- Typeface_Delegate.resetDefaults();
- Typeface.sDynamicTypefaceCache.evictAll();
- sProject9PatchCache.clear();
- sProjectBitmapCache.clear();
-
- return true;
- }
-
- /**
- * Starts a layout session by inflating and rendering it. The method returns a
- * {@link RenderSession} on which further actions can be taken.
- * <p/>
- * If {@link SessionParams} includes the {@link RenderParamsFlags#FLAG_DO_NOT_RENDER_ON_CREATE},
- * this method will only inflate the layout but will NOT render it.
- * @param params the {@link SessionParams} object with all the information necessary to create
- * the scene.
- * @return a new {@link RenderSession} object that contains the result of the layout.
- * @since 5
- */
- @Override
public RenderSession createSession(SessionParams params) {
- try {
- Result lastResult;
- RenderSessionImpl scene = new RenderSessionImpl(params);
- try {
- prepareThread();
- lastResult = scene.init(params.getTimeout());
- if (lastResult.isSuccess()) {
- lastResult = scene.inflate();
-
- boolean doNotRenderOnCreate = Boolean.TRUE.equals(
- params.getFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE));
- if (lastResult.isSuccess() && !doNotRenderOnCreate) {
- lastResult = scene.render(true /*freshRender*/);
- }
- }
- } finally {
- scene.release();
- cleanupThread();
- }
-
- return new BridgeRenderSession(scene, lastResult);
- } catch (Throwable t) {
- // get the real cause of the exception.
- Throwable t2 = t;
- while (t2.getCause() != null) {
- t2 = t2.getCause();
- }
- return new BridgeRenderSession(null,
- ERROR_UNKNOWN.createResult(t2.getMessage(), t));
- }
- }
-
- @Override
- public Result renderDrawable(DrawableParams params) {
- try {
- Result lastResult;
- RenderDrawable action = new RenderDrawable(params);
- try {
- prepareThread();
- lastResult = action.init(params.getTimeout());
- if (lastResult.isSuccess()) {
- lastResult = action.render();
- }
- } finally {
- action.release();
- cleanupThread();
- }
-
- return lastResult;
- } catch (Throwable t) {
- // get the real cause of the exception.
- Throwable t2 = t;
- while (t2.getCause() != null) {
- t2 = t.getCause();
- }
- return ERROR_UNKNOWN.createResult(t2.getMessage(), t);
- }
+ return new BridgeRenderSession();
}
@Override
- public void clearCaches(Object projectKey) {
- if (projectKey != null) {
- sProjectBitmapCache.remove(projectKey);
- sProject9PatchCache.remove(projectKey);
- }
- }
-
- @Override
- public Result getViewParent(Object viewObject) {
- if (viewObject instanceof View) {
- return Status.SUCCESS.createResult(((View)viewObject).getParent());
- }
-
- throw new IllegalArgumentException("viewObject is not a View");
- }
-
- @Override
- public Result getViewIndex(Object viewObject) {
- if (viewObject instanceof View) {
- View view = (View) viewObject;
- ViewParent parentView = view.getParent();
-
- if (parentView instanceof ViewGroup) {
- Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view));
- }
-
- return Status.SUCCESS.createResult();
- }
-
- throw new IllegalArgumentException("viewObject is not a View");
- }
-
- @Override
- public boolean isRtl(String locale) {
- return isLocaleRtl(locale);
- }
-
- public static boolean isLocaleRtl(String locale) {
- if (locale == null) {
- locale = "";
- }
- ULocale uLocale = new ULocale(locale);
- return uLocale.getCharacterOrientation().equals(ICU_LOCALE_DIRECTION_RTL);
- }
-
- /**
- * Returns the lock for the bridge
- */
- public static ReentrantLock getLock() {
- return sLock;
- }
-
- /**
- * Prepares the current thread for rendering.
- *
- * Note that while this can be called several time, the first call to {@link #cleanupThread()}
- * will do the clean-up, and make the thread unable to do further scene actions.
- */
- public synchronized static void prepareThread() {
- // we need to make sure the Looper has been initialized for this thread.
- // this is required for View that creates Handler objects.
- if (Looper.myLooper() == null) {
- Looper.prepareMainLooper();
- }
- }
-
- /**
- * Cleans up thread-specific data. After this, the thread cannot be used for scene actions.
- * <p>
- * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single
- * call to this will prevent the thread from doing further scene actions
- */
- public synchronized static void cleanupThread() {
- // clean up the looper
- Looper_Accessor.cleanupThread();
- }
-
- public static LayoutLog getLog() {
- return sCurrentLog;
- }
-
- public static void setLog(LayoutLog log) {
- // check only the thread currently owning the lock can do this.
- if (!sLock.isHeldByCurrentThread()) {
- throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
- }
-
- if (log != null) {
- sCurrentLog = log;
- } else {
- sCurrentLog = sDefaultLog;
- }
- }
-
- /**
- * Returns details of a framework resource from its integer value.
- * @param value the integer value
- * @return a Pair containing the resource type and name, or null if the id
- * does not match any resource.
- */
- @SuppressWarnings("deprecation")
- public static Pair<ResourceType, String> resolveResourceId(int value) {
- Pair<ResourceType, String> pair = sRMap.get(value);
- if (pair == null) {
- pair = sDynamicIds.resolveId(value);
- }
- return pair;
- }
-
- /**
- * Returns the integer id of a framework resource, from a given resource type and resource name.
- * <p/>
- * If no resource is found, it creates a dynamic id for the resource.
- *
- * @param type the type of the resource
- * @param name the name of the resource.
- *
- * @return an {@link Integer} containing the resource id.
- */
- @NonNull
- public static Integer getResourceId(ResourceType type, String name) {
- Map<String, Integer> map = sRevRMap.get(type);
- Integer value = null;
- if (map != null) {
- value = map.get(name);
- }
-
- return value == null ? sDynamicIds.getId(type, name) : value;
-
- }
-
- /**
- * Returns the list of possible enums for a given attribute name.
- */
- public static Map<String, Integer> getEnumValues(String attributeName) {
- if (sEnumValueMap != null) {
- return sEnumValueMap.get(attributeName);
- }
-
- return null;
- }
-
- /**
- * Returns the platform build properties.
- */
- public static Map<String, String> getPlatformProperties() {
- return sPlatformProperties;
- }
-
- /**
- * Returns the bitmap for a specific path, from a specific project cache, or from the
- * framework cache.
- * @param value the path of the bitmap
- * @param projectKey the key of the project, or null to query the framework cache.
- * @return the cached Bitmap or null if not found.
- */
- public static Bitmap getCachedBitmap(String value, Object projectKey) {
- if (projectKey != null) {
- Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
- if (map != null) {
- SoftReference<Bitmap> ref = map.get(value);
- if (ref != null) {
- return ref.get();
- }
- }
- } else {
- SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value);
- if (ref != null) {
- return ref.get();
- }
- }
-
- return null;
- }
-
- /**
- * Sets a bitmap in a project cache or in the framework cache.
- * @param value the path of the bitmap
- * @param bmp the Bitmap object
- * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
- */
- public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) {
- if (projectKey != null) {
- Map<String, SoftReference<Bitmap>> map =
- sProjectBitmapCache.computeIfAbsent(projectKey, k -> new HashMap<>());
-
- map.put(value, new SoftReference<>(bmp));
- } else {
- sFrameworkBitmapCache.put(value, new SoftReference<>(bmp));
- }
- }
-
- /**
- * Returns the 9 patch chunk for a specific path, from a specific project cache, or from the
- * framework cache.
- * @param value the path of the 9 patch
- * @param projectKey the key of the project, or null to query the framework cache.
- * @return the cached 9 patch or null if not found.
- */
- public static NinePatchChunk getCached9Patch(String value, Object projectKey) {
- if (projectKey != null) {
- Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
-
- if (map != null) {
- SoftReference<NinePatchChunk> ref = map.get(value);
- if (ref != null) {
- return ref.get();
- }
- }
- } else {
- SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value);
- if (ref != null) {
- return ref.get();
- }
- }
-
- return null;
- }
-
- /**
- * Sets a 9 patch chunk in a project cache or in the framework cache.
- * @param value the path of the 9 patch
- * @param ninePatch the 9 patch object
- * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
- */
- public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) {
- if (projectKey != null) {
- Map<String, SoftReference<NinePatchChunk>> map =
- sProject9PatchCache.computeIfAbsent(projectKey, k -> new HashMap<>());
-
- map.put(value, new SoftReference<>(ninePatch));
- } else {
- sFramework9PatchCache.put(value, new SoftReference<>(ninePatch));
- }
+ public int getApiLevel() {
+ return 0;
}
}
diff --git a/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java b/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java
index 4805ed1e..c38e9e83 100644
--- a/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java
+++ b/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java
@@ -154,12 +154,6 @@ public class BridgeIInputMethodManager implements IInputMethodManager {
}
@Override
- public boolean setInputMethodEnabled(String arg0, boolean arg1) throws RemoteException {
- // TODO Auto-generated method stub
- return false;
- }
-
- @Override
public void showInputMethodAndSubtypeEnablerFromClient(IInputMethodClient arg0, String arg1)
throws RemoteException {
// TODO Auto-generated method stub
@@ -232,7 +226,8 @@ public class BridgeIInputMethodManager implements IInputMethodManager {
IInputMethodClient client, IBinder windowToken, int controlFlags,
/* @android.view.WindowManager.LayoutParams.SoftInputModeFlags */ int softInputMode,
int windowFlags, EditorInfo attribute, IInputContext inputContext,
- /* @InputConnectionInspector.MissingMethodFlags */ int missingMethodFlags)
+ /* @InputConnectionInspector.MissingMethodFlags */ int missingMethodFlags,
+ int unverifiedTargetSdkVersion)
throws RemoteException {
// TODO Auto-generated method stub
return null;
@@ -255,4 +250,10 @@ public class BridgeIInputMethodManager implements IInputMethodManager {
public void reportFullscreenMode(IBinder token, boolean fullscreen) {
// TODO Auto-generated method stub
}
+
+ @Override
+ public List<InputMethodInfo> getVrInputMethodList() {
+ // TODO Auto-generated method stub.
+ return null;
+ }
}
diff --git a/com/android/layoutlib/bridge/android/BridgeWindow.java b/com/android/layoutlib/bridge/android/BridgeWindow.java
index ffbe7c43..b3226b34 100644
--- a/com/android/layoutlib/bridge/android/BridgeWindow.java
+++ b/com/android/layoutlib/bridge/android/BridgeWindow.java
@@ -24,6 +24,7 @@ import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.MergedConfiguration;
+import android.view.DisplayCutout;
import android.view.DragEvent;
import android.view.IWindow;
@@ -50,8 +51,8 @@ public final class BridgeWindow implements IWindow {
@Override
public void resized(Rect rect, Rect rect2, Rect rect3, Rect rect4, Rect rect5, Rect rect6,
- boolean b, MergedConfiguration mergedConfig, Rect rect7, boolean b2, boolean b3, int i0)
- throws RemoteException {
+ boolean b, MergedConfiguration mergedConfig, Rect rect7, boolean b2, boolean b3, int i0,
+ DisplayCutout.ParcelableWrapper displayCutout) throws RemoteException {
// pass for now.
}
diff --git a/com/android/layoutlib/bridge/android/BridgeWindowSession.java b/com/android/layoutlib/bridge/android/BridgeWindowSession.java
index 2c883940..bf37d637 100644
--- a/com/android/layoutlib/bridge/android/BridgeWindowSession.java
+++ b/com/android/layoutlib/bridge/android/BridgeWindowSession.java
@@ -23,6 +23,7 @@ import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.MergedConfiguration;
+import android.view.DisplayCutout;
import android.view.IWindow;
import android.view.IWindowId;
import android.view.IWindowSession;
@@ -47,8 +48,8 @@ public final class BridgeWindowSession implements IWindowSession {
@Override
public int addToDisplay(IWindow arg0, int seq, LayoutParams arg1, int arg2, int displayId,
- Rect arg3, Rect arg4, Rect arg5, InputChannel outInputchannel)
- throws RemoteException {
+ Rect arg3, Rect arg4, Rect arg5, DisplayCutout.ParcelableWrapper displayCutout,
+ InputChannel outInputchannel) throws RemoteException {
// pass for now.
return 0;
}
@@ -89,7 +90,8 @@ public final class BridgeWindowSession implements IWindowSession {
@Override
public int relayout(IWindow iWindow, int i, LayoutParams layoutParams, int i2,
int i3, int i4, int i5, Rect rect, Rect rect2, Rect rect3, Rect rect4, Rect rect5,
- Rect rect6, Rect rect7, MergedConfiguration mergedConfig, Surface surface)
+ Rect rect6, Rect rect7, DisplayCutout.ParcelableWrapper displayCutout,
+ MergedConfiguration mergedConfig, Surface surface)
throws RemoteException {
// pass for now.
return 0;
diff --git a/com/android/providers/settings/SettingsProtoDumpUtil.java b/com/android/providers/settings/SettingsProtoDumpUtil.java
index 52b4f4df..1d3f26ee 100644
--- a/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -881,6 +881,9 @@ class SettingsProtoDumpUtil {
dumpSetting(s, p,
Settings.Global.GPU_DEBUG_LAYERS,
GlobalSettingsProto.GPU_DEBUG_LAYERS);
+ dumpSetting(s, p,
+ Settings.Global.ENABLE_GNSS_RAW_MEAS_FULL_TRACKING,
+ GlobalSettingsProto.ENABLE_GNSS_RAW_MEAS_FULL_TRACKING);
// Settings.Global.SHOW_PROCESSES intentionally excluded since it's deprecated.
dumpSetting(s, p,
Settings.Global.LOW_POWER_MODE,
@@ -1057,9 +1060,6 @@ class SettingsProtoDumpUtil {
Settings.Global.LOCATION_SETTINGS_LINK_TO_PERMISSIONS_ENABLED,
GlobalSettingsProto.LOCATION_SETTINGS_LINK_TO_PERMISSIONS_ENABLED);
dumpSetting(s, p,
- Settings.Global.BACKUP_REFACTORED_SERVICE_DISABLED,
- GlobalSettingsProto.BACKUP_REFACTORED_SERVICE_DISABLED);
- dumpSetting(s, p,
Settings.Global.EUICC_FACTORY_RESET_TIMEOUT_MILLIS,
GlobalSettingsProto.EUICC_FACTORY_RESET_TIMEOUT_MILLIS);
dumpSetting(s, p,
diff --git a/com/android/providers/settings/SettingsProvider.java b/com/android/providers/settings/SettingsProvider.java
index 7fb6ede8..bef2bcbd 100644
--- a/com/android/providers/settings/SettingsProvider.java
+++ b/com/android/providers/settings/SettingsProvider.java
@@ -174,13 +174,10 @@ public class SettingsProvider extends ContentProvider {
Settings.NameValueTable.VALUE
};
- public static final int SETTINGS_TYPE_GLOBAL = 0;
- public static final int SETTINGS_TYPE_SYSTEM = 1;
- public static final int SETTINGS_TYPE_SECURE = 2;
- public static final int SETTINGS_TYPE_SSAID = 3;
-
- public static final int SETTINGS_TYPE_MASK = 0xF0000000;
- public static final int SETTINGS_TYPE_SHIFT = 28;
+ public static final int SETTINGS_TYPE_GLOBAL = SettingsState.SETTINGS_TYPE_GLOBAL;
+ public static final int SETTINGS_TYPE_SYSTEM = SettingsState.SETTINGS_TYPE_SYSTEM;
+ public static final int SETTINGS_TYPE_SECURE = SettingsState.SETTINGS_TYPE_SECURE;
+ public static final int SETTINGS_TYPE_SSAID = SettingsState.SETTINGS_TYPE_SSAID;
private static final Bundle NULL_SETTING_BUNDLE = Bundle.forPair(
Settings.NameValueTable.VALUE, null);
@@ -278,40 +275,23 @@ public class SettingsProvider extends ContentProvider {
private volatile IPackageManager mPackageManager;
public static int makeKey(int type, int userId) {
- return (type << SETTINGS_TYPE_SHIFT) | userId;
+ return SettingsState.makeKey(type, userId);
}
public static int getTypeFromKey(int key) {
- return key >>> SETTINGS_TYPE_SHIFT;
+ return SettingsState.getTypeFromKey(key);
}
public static int getUserIdFromKey(int key) {
- return key & ~SETTINGS_TYPE_MASK;
+ return SettingsState.getUserIdFromKey(key);
}
public static String settingTypeToString(int type) {
- switch (type) {
- case SETTINGS_TYPE_GLOBAL: {
- return "SETTINGS_GLOBAL";
- }
- case SETTINGS_TYPE_SECURE: {
- return "SETTINGS_SECURE";
- }
- case SETTINGS_TYPE_SYSTEM: {
- return "SETTINGS_SYSTEM";
- }
- case SETTINGS_TYPE_SSAID: {
- return "SETTINGS_SSAID";
- }
- default: {
- return "UNKNOWN";
- }
- }
+ return SettingsState.settingTypeToString(type);
}
public static String keyToString(int key) {
- return "Key[user=" + getUserIdFromKey(key) + ";type="
- + settingTypeToString(getTypeFromKey(key)) + "]";
+ return SettingsState.keyToString(key);
}
@Override
@@ -1730,18 +1710,9 @@ public class SettingsProvider extends ContentProvider {
}
private List<String> getSettingsNamesLocked(int settingsType, int userId) {
- boolean instantApp;
- if (UserHandle.getAppId(Binder.getCallingUid()) < Process.FIRST_APPLICATION_UID) {
- instantApp = false;
- } else {
- ApplicationInfo ai = getCallingApplicationInfoOrThrow();
- instantApp = ai.isInstantApp();
- }
- if (instantApp) {
- return new ArrayList<String>(getInstantAppAccessibleSettings(settingsType));
- } else {
- return mSettingsRegistry.getSettingsNamesLocked(settingsType, userId);
- }
+ // Don't enforce the instant app whitelist for now -- its too prone to unintended breakage
+ // in the current form.
+ return mSettingsRegistry.getSettingsNamesLocked(settingsType, userId);
}
private void enforceSettingReadable(String settingName, int settingsType, int userId) {
@@ -1754,8 +1725,10 @@ public class SettingsProvider extends ContentProvider {
}
if (!getInstantAppAccessibleSettings(settingsType).contains(settingName)
&& !getOverlayInstantAppAccessibleSettings(settingsType).contains(settingName)) {
- throw new SecurityException("Setting " + settingName + " is not accessible from"
- + " ephemeral package " + getCallingPackage());
+ // Don't enforce the instant app whitelist for now -- its too prone to unintended
+ // breakage in the current form.
+ Slog.w(LOG_TAG, "Instant App " + ai.packageName
+ + " trying to access unexposed setting, this will be an error in the future.");
}
}
diff --git a/com/android/providers/settings/SettingsState.java b/com/android/providers/settings/SettingsState.java
index f901bcae..a8a67aba 100644
--- a/com/android/providers/settings/SettingsState.java
+++ b/com/android/providers/settings/SettingsState.java
@@ -33,6 +33,7 @@ import android.os.Message;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
+import android.provider.Settings.Global;
import android.providers.settings.GlobalSettingsProto;
import android.providers.settings.SettingsOperationProto;
import android.text.TextUtils;
@@ -46,6 +47,7 @@ import android.util.Xml;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import libcore.io.IoUtils;
@@ -195,6 +197,51 @@ final class SettingsState {
@GuardedBy("mLock")
private int mNextHistoricalOpIdx;
+ public static final int SETTINGS_TYPE_GLOBAL = 0;
+ public static final int SETTINGS_TYPE_SYSTEM = 1;
+ public static final int SETTINGS_TYPE_SECURE = 2;
+ public static final int SETTINGS_TYPE_SSAID = 3;
+
+ public static final int SETTINGS_TYPE_MASK = 0xF0000000;
+ public static final int SETTINGS_TYPE_SHIFT = 28;
+
+ public static int makeKey(int type, int userId) {
+ return (type << SETTINGS_TYPE_SHIFT) | userId;
+ }
+
+ public static int getTypeFromKey(int key) {
+ return key >>> SETTINGS_TYPE_SHIFT;
+ }
+
+ public static int getUserIdFromKey(int key) {
+ return key & ~SETTINGS_TYPE_MASK;
+ }
+
+ public static String settingTypeToString(int type) {
+ switch (type) {
+ case SETTINGS_TYPE_GLOBAL: {
+ return "SETTINGS_GLOBAL";
+ }
+ case SETTINGS_TYPE_SECURE: {
+ return "SETTINGS_SECURE";
+ }
+ case SETTINGS_TYPE_SYSTEM: {
+ return "SETTINGS_SYSTEM";
+ }
+ case SETTINGS_TYPE_SSAID: {
+ return "SETTINGS_SSAID";
+ }
+ default: {
+ return "UNKNOWN";
+ }
+ }
+ }
+
+ public static String keyToString(int key) {
+ return "Key[user=" + getUserIdFromKey(key) + ";type="
+ + settingTypeToString(getTypeFromKey(key)) + "]";
+ }
+
public SettingsState(Context context, Object lock, File file, int key,
int maxBytesPerAppPackage, Looper looper) {
// It is important that we use the same lock as the settings provider
@@ -604,6 +651,13 @@ final class SettingsState {
for (int i = 0; i < settingCount; i++) {
Setting setting = settings.valueAt(i);
+ if (setting.isTransient()) {
+ if (DEBUG_PERSISTENCE) {
+ Slog.i(LOG_TAG, "[SKIPPED PERSISTING]" + setting.getName());
+ }
+ continue;
+ }
+
writeSingleSetting(mVersion, serializer, setting.getId(), setting.getName(),
setting.getValue(), setting.getDefaultValue(), setting.getPackageName(),
setting.getTag(), setting.isDefaultFromSystem());
@@ -914,6 +968,14 @@ final class SettingsState {
return update(this.defaultValue, false, packageName, null, true);
}
+ public boolean isTransient() {
+ switch (getTypeFromKey(getKey())) {
+ case SETTINGS_TYPE_GLOBAL:
+ return ArrayUtils.contains(Global.TRANSIENT_SETTINGS, getName());
+ }
+ return false;
+ }
+
public boolean update(String value, boolean setDefault, String packageName, String tag,
boolean forceNonSystemPackage) {
if (NULL_VALUE.equals(value)) {
diff --git a/com/android/server/AlarmManagerInternal.java b/com/android/server/AlarmManagerInternal.java
new file mode 100644
index 00000000..dbff957a
--- /dev/null
+++ b/com/android/server/AlarmManagerInternal.java
@@ -0,0 +1,21 @@
+/*
+ * 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 com.android.server;
+
+public interface AlarmManagerInternal {
+ void removeAlarmsForUid(int uid);
+}
diff --git a/com/android/server/AlarmManagerService.java b/com/android/server/AlarmManagerService.java
index ca152495..06cf9820 100644
--- a/com/android/server/AlarmManagerService.java
+++ b/com/android/server/AlarmManagerService.java
@@ -92,6 +92,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.LocalLog;
import com.android.server.ForceAppStandbyTracker.Listener;
+import com.android.server.LocalServices;
/**
* Alarm manager implementaion.
@@ -467,21 +468,14 @@ class AlarmManagerService extends SystemService {
return newStart;
}
- boolean remove(final PendingIntent operation, final IAlarmListener listener) {
- if (operation == null && listener == null) {
- if (localLOGV) {
- Slog.w(TAG, "requested remove() of null operation",
- new RuntimeException("here"));
- }
- return false;
- }
+ boolean remove(Predicate<Alarm> predicate) {
boolean didRemove = false;
long newStart = 0; // recalculate endpoints as we go
long newEnd = Long.MAX_VALUE;
int newFlags = 0;
for (int i = 0; i < alarms.size(); ) {
Alarm alarm = alarms.get(i);
- if (alarm.matches(operation, listener)) {
+ if (predicate.test(alarm)) {
alarms.remove(i);
didRemove = true;
if (alarm.alarmClock != null) {
@@ -507,111 +501,6 @@ class AlarmManagerService extends SystemService {
return didRemove;
}
- boolean remove(final String packageName) {
- if (packageName == null) {
- if (localLOGV) {
- Slog.w(TAG, "requested remove() of null packageName",
- new RuntimeException("here"));
- }
- return false;
- }
- boolean didRemove = false;
- long newStart = 0; // recalculate endpoints as we go
- long newEnd = Long.MAX_VALUE;
- int newFlags = 0;
- for (int i = alarms.size()-1; i >= 0; i--) {
- Alarm alarm = alarms.get(i);
- if (alarm.matches(packageName)) {
- alarms.remove(i);
- didRemove = true;
- if (alarm.alarmClock != null) {
- mNextAlarmClockMayChange = true;
- }
- } else {
- if (alarm.whenElapsed > newStart) {
- newStart = alarm.whenElapsed;
- }
- if (alarm.maxWhenElapsed < newEnd) {
- newEnd = alarm.maxWhenElapsed;
- }
- newFlags |= alarm.flags;
- }
- }
- if (didRemove) {
- // commit the new batch bounds
- start = newStart;
- end = newEnd;
- flags = newFlags;
- }
- return didRemove;
- }
-
- boolean removeForStopped(final int uid) {
- boolean didRemove = false;
- long newStart = 0; // recalculate endpoints as we go
- long newEnd = Long.MAX_VALUE;
- int newFlags = 0;
- for (int i = alarms.size()-1; i >= 0; i--) {
- Alarm alarm = alarms.get(i);
- try {
- if (alarm.uid == uid && ActivityManager.getService().isAppStartModeDisabled(
- uid, alarm.packageName)) {
- alarms.remove(i);
- didRemove = true;
- if (alarm.alarmClock != null) {
- mNextAlarmClockMayChange = true;
- }
- } else {
- if (alarm.whenElapsed > newStart) {
- newStart = alarm.whenElapsed;
- }
- if (alarm.maxWhenElapsed < newEnd) {
- newEnd = alarm.maxWhenElapsed;
- }
- newFlags |= alarm.flags;
- }
- } catch (RemoteException e) {
- }
- }
- if (didRemove) {
- // commit the new batch bounds
- start = newStart;
- end = newEnd;
- flags = newFlags;
- }
- return didRemove;
- }
-
- boolean remove(final int userHandle) {
- boolean didRemove = false;
- long newStart = 0; // recalculate endpoints as we go
- long newEnd = Long.MAX_VALUE;
- for (int i = 0; i < alarms.size(); ) {
- Alarm alarm = alarms.get(i);
- if (UserHandle.getUserId(alarm.creatorUid) == userHandle) {
- alarms.remove(i);
- didRemove = true;
- if (alarm.alarmClock != null) {
- mNextAlarmClockMayChange = true;
- }
- } else {
- if (alarm.whenElapsed > newStart) {
- newStart = alarm.whenElapsed;
- }
- if (alarm.maxWhenElapsed < newEnd) {
- newEnd = alarm.maxWhenElapsed;
- }
- i++;
- }
- }
- if (didRemove) {
- // commit the new batch bounds
- start = newStart;
- end = newEnd;
- }
- return didRemove;
- }
-
boolean hasPackage(final String packageName) {
final int N = alarms.size();
for (int i = 0; i < N; i++) {
@@ -759,6 +648,8 @@ class AlarmManagerService extends SystemService {
mForceAppStandbyTracker = ForceAppStandbyTracker.getInstance(context);
mForceAppStandbyTracker.addListener(mForceAppStandbyListener);
+
+ publishLocalService(AlarmManagerInternal.class, new LocalService());
}
static long convertToElapsed(long when, int type) {
@@ -1554,6 +1445,21 @@ class AlarmManagerService extends SystemService {
}
}
+ /**
+ * System-process internal API
+ */
+ private final class LocalService implements AlarmManagerInternal {
+ @Override
+ public void removeAlarmsForUid(int uid) {
+ synchronized (mLock) {
+ removeLocked(uid);
+ }
+ }
+ }
+
+ /**
+ * Public-facing binder interface
+ */
private final IBinder mService = new IAlarmManager.Stub() {
@Override
public void set(String callingPackage,
@@ -2430,10 +2336,19 @@ class AlarmManagerService extends SystemService {
}
private void removeLocked(PendingIntent operation, IAlarmListener directReceiver) {
+ if (operation == null && directReceiver == null) {
+ if (localLOGV) {
+ Slog.w(TAG, "requested remove() of null operation",
+ new RuntimeException("here"));
+ }
+ return;
+ }
+
boolean didRemove = false;
+ final Predicate<Alarm> whichAlarms = (Alarm a) -> a.matches(operation, directReceiver);
for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
Batch b = mAlarmBatches.get(i);
- didRemove |= b.remove(operation, directReceiver);
+ didRemove |= b.remove(whichAlarms);
if (b.size() == 0) {
mAlarmBatches.remove(i);
}
@@ -2476,11 +2391,58 @@ class AlarmManagerService extends SystemService {
}
}
- void removeLocked(String packageName) {
+ void removeLocked(final int uid) {
boolean didRemove = false;
+ final Predicate<Alarm> whichAlarms = (Alarm a) -> a.uid == uid;
for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
Batch b = mAlarmBatches.get(i);
- didRemove |= b.remove(packageName);
+ didRemove |= b.remove(whichAlarms);
+ if (b.size() == 0) {
+ mAlarmBatches.remove(i);
+ }
+ }
+ for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
+ final Alarm a = mPendingWhileIdleAlarms.get(i);
+ if (a.uid == uid) {
+ // Don't set didRemove, since this doesn't impact the scheduled alarms.
+ mPendingWhileIdleAlarms.remove(i);
+ }
+ }
+ for (int i = mPendingBackgroundAlarms.size() - 1; i >= 0; i --) {
+ final ArrayList<Alarm> alarmsForUid = mPendingBackgroundAlarms.valueAt(i);
+ for (int j = alarmsForUid.size() - 1; j >= 0; j--) {
+ if (alarmsForUid.get(j).uid == uid) {
+ alarmsForUid.remove(j);
+ }
+ }
+ if (alarmsForUid.size() == 0) {
+ mPendingBackgroundAlarms.removeAt(i);
+ }
+ }
+ if (didRemove) {
+ if (DEBUG_BATCH) {
+ Slog.v(TAG, "remove(uid) changed bounds; rebatching");
+ }
+ rebatchAllAlarmsLocked(true);
+ rescheduleKernelAlarmsLocked();
+ updateNextAlarmClockLocked();
+ }
+ }
+
+ void removeLocked(final String packageName) {
+ if (packageName == null) {
+ if (localLOGV) {
+ Slog.w(TAG, "requested remove() of null packageName",
+ new RuntimeException("here"));
+ }
+ return;
+ }
+
+ boolean didRemove = false;
+ final Predicate<Alarm> whichAlarms = (Alarm a) -> a.matches(packageName);
+ for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
+ Batch b = mAlarmBatches.get(i);
+ didRemove |= b.remove(whichAlarms);
if (b.size() == 0) {
mAlarmBatches.remove(i);
}
@@ -2513,11 +2475,20 @@ class AlarmManagerService extends SystemService {
}
}
- void removeForStoppedLocked(int uid) {
+ void removeForStoppedLocked(final int uid) {
boolean didRemove = false;
+ final Predicate<Alarm> whichAlarms = (Alarm a) -> {
+ try {
+ if (a.uid == uid && ActivityManager.getService().isAppStartModeDisabled(
+ uid, a.packageName)) {
+ return true;
+ }
+ } catch (RemoteException e) { /* fall through */}
+ return false;
+ };
for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
Batch b = mAlarmBatches.get(i);
- didRemove |= b.removeForStopped(uid);
+ didRemove |= b.remove(whichAlarms);
if (b.size() == 0) {
mAlarmBatches.remove(i);
}
@@ -2546,9 +2517,11 @@ class AlarmManagerService extends SystemService {
void removeUserLocked(int userHandle) {
boolean didRemove = false;
+ final Predicate<Alarm> whichAlarms =
+ (Alarm a) -> UserHandle.getUserId(a.creatorUid) == userHandle;
for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
Batch b = mAlarmBatches.get(i);
- didRemove |= b.remove(userHandle);
+ didRemove |= b.remove(whichAlarms);
if (b.size() == 0) {
mAlarmBatches.remove(i);
}
@@ -2967,7 +2940,7 @@ class AlarmManagerService extends SystemService {
proto.write(AlarmProto.TAG, statsTag);
proto.write(AlarmProto.TYPE, type);
- proto.write(AlarmProto.WHEN_ELAPSED_MS, whenElapsed - nowElapsed);
+ proto.write(AlarmProto.TIME_UNTIL_WHEN_ELAPSED_MS, whenElapsed - nowElapsed);
proto.write(AlarmProto.WINDOW_LENGTH_MS, windowLength);
proto.write(AlarmProto.REPEAT_INTERVAL_MS, repeatInterval);
proto.write(AlarmProto.COUNT, count);
@@ -3396,6 +3369,7 @@ class AlarmManagerService extends SystemService {
@Override
public void onReceive(Context context, Intent intent) {
+ final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
synchronized (mLock) {
String action = intent.getAction();
String pkgList[] = null;
@@ -3416,7 +3390,6 @@ class AlarmManagerService extends SystemService {
removeUserLocked(userHandle);
}
} else if (Intent.ACTION_UID_REMOVED.equals(action)) {
- int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
if (uid >= 0) {
mLastAllowWhileIdleDispatch.delete(uid);
}
@@ -3436,7 +3409,13 @@ class AlarmManagerService extends SystemService {
}
if (pkgList != null && (pkgList.length > 0)) {
for (String pkg : pkgList) {
- removeLocked(pkg);
+ if (uid >= 0) {
+ // package-removed case
+ removeLocked(uid);
+ } else {
+ // external-applications-unavailable etc case
+ removeLocked(pkg);
+ }
mPriorities.remove(pkg);
for (int i=mBroadcastStats.size()-1; i>=0; i--) {
ArrayMap<String, BroadcastStats> uidStats = mBroadcastStats.valueAt(i);
diff --git a/com/android/server/AnimationThread.java b/com/android/server/AnimationThread.java
index 08392b06..c86042b2 100644
--- a/com/android/server/AnimationThread.java
+++ b/com/android/server/AnimationThread.java
@@ -22,8 +22,8 @@ import android.os.Handler;
import android.os.Trace;
/**
- * Thread for handling all window animations, or anything that's directly impacting animations like
- * starting windows or traversals.
+ * Thread for handling all legacy window animations, or anything that's directly impacting
+ * animations like starting windows or traversals.
*/
public final class AnimationThread extends ServiceThread {
private static AnimationThread sInstance;
diff --git a/com/android/server/BatteryService.java b/com/android/server/BatteryService.java
index 924e736b..04d292fa 100644
--- a/com/android/server/BatteryService.java
+++ b/com/android/server/BatteryService.java
@@ -421,7 +421,9 @@ public final class BatteryService extends SystemService {
boolean logOutlier = false;
long dischargeDuration = 0;
- mBatteryLevelCritical = (mHealthInfo.batteryLevel <= mCriticalBatteryLevel);
+ mBatteryLevelCritical =
+ mHealthInfo.batteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN
+ && mHealthInfo.batteryLevel <= mCriticalBatteryLevel;
if (mHealthInfo.chargerAcOnline) {
mPlugType = BatteryManager.BATTERY_PLUGGED_AC;
} else if (mHealthInfo.chargerUsbOnline) {
@@ -509,6 +511,8 @@ public final class BatteryService extends SystemService {
if (!mBatteryLevelLow) {
// Should we now switch in to low battery mode?
if (mPlugType == BATTERY_PLUGGED_NONE
+ && mHealthInfo.batteryStatus !=
+ BatteryManager.BATTERY_STATUS_UNKNOWN
&& mHealthInfo.batteryLevel <= mLowBatteryWarningLevel) {
mBatteryLevelLow = true;
}
diff --git a/com/android/server/BluetoothManagerService.java b/com/android/server/BluetoothManagerService.java
index 04279a31..d9713a51 100644
--- a/com/android/server/BluetoothManagerService.java
+++ b/com/android/server/BluetoothManagerService.java
@@ -67,6 +67,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.LinkedList;
+import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -79,15 +80,14 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN;
private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH;
- private static final String SECURE_SETTINGS_BLUETOOTH_ADDR_VALID="bluetooth_addr_valid";
- private static final String SECURE_SETTINGS_BLUETOOTH_ADDRESS="bluetooth_address";
- private static final String SECURE_SETTINGS_BLUETOOTH_NAME="bluetooth_name";
+ private static final String SECURE_SETTINGS_BLUETOOTH_ADDR_VALID = "bluetooth_addr_valid";
+ private static final String SECURE_SETTINGS_BLUETOOTH_ADDRESS = "bluetooth_address";
+ private static final String SECURE_SETTINGS_BLUETOOTH_NAME = "bluetooth_name";
private static final int ACTIVE_LOG_MAX_SIZE = 20;
private static final int CRASH_LOG_MAX_SIZE = 100;
private static final String REASON_AIRPLANE_MODE = "airplane mode";
private static final String REASON_DISALLOWED = "disallowed by system";
- private static final String REASON_SHARING_DISALLOWED = "sharing disallowed by system";
private static final String REASON_RESTARTED = "automatic restart";
private static final String REASON_START_CRASH = "turn-on crash";
private static final String REASON_SYSTEM_BOOT = "system boot";
@@ -130,14 +130,14 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private static final int MAX_ERROR_RESTART_RETRIES = 6;
// Bluetooth persisted setting is off
- private static final int BLUETOOTH_OFF=0;
+ private static final int BLUETOOTH_OFF = 0;
// Bluetooth persisted setting is on
// and Airplane mode won't affect Bluetooth state at start up
- private static final int BLUETOOTH_ON_BLUETOOTH=1;
+ private static final int BLUETOOTH_ON_BLUETOOTH = 1;
// Bluetooth persisted setting is on
// but Airplane mode will affect Bluetooth state at start up
// and Airplane mode will have higher priority.
- private static final int BLUETOOTH_ON_AIRPLANE=2;
+ private static final int BLUETOOTH_ON_AIRPLANE = 2;
private static final int SERVICE_IBLUETOOTH = 1;
private static final int SERVICE_IBLUETOOTHGATT = 2;
@@ -154,8 +154,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private IBinder mBluetoothBinder;
private IBluetooth mBluetooth;
private IBluetoothGatt mBluetoothGatt;
- private final ReentrantReadWriteLock mBluetoothLock =
- new ReentrantReadWriteLock();
+ private final ReentrantReadWriteLock mBluetoothLock = new ReentrantReadWriteLock();
private boolean mBinding;
private boolean mUnbinding;
@@ -175,7 +174,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private boolean mEnable;
private long mTimestamp;
- public ActiveLog(String packageName, boolean enable, long timestamp) {
+ ActiveLog(String packageName, boolean enable, long timestamp) {
mPackageName = packageName;
mEnable = enable;
mTimestamp = timestamp;
@@ -186,8 +185,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
public String toString() {
- return timeToLog(mTimestamp) + (mEnable ? " Enabled " : " Disabled ") + " by "
- + mPackageName;
+ return timeToLog(mTimestamp) + (mEnable ? " Enabled " : " Disabled ") + " by "
+ + mPackageName;
}
}
@@ -203,7 +202,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private boolean mEnableExternal;
// Map of apps registered to keep BLE scanning on.
- private Map<IBinder, ClientDeathRecipient> mBleApps = new ConcurrentHashMap<IBinder, ClientDeathRecipient>();
+ private Map<IBinder, ClientDeathRecipient> mBleApps =
+ new ConcurrentHashMap<IBinder, ClientDeathRecipient>();
private int mState;
private final BluetoothHandler mHandler;
@@ -212,51 +212,52 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
// Save a ProfileServiceConnections object for each of the bound
// bluetooth profile services
- private final Map <Integer, ProfileServiceConnections> mProfileServices =
- new HashMap <Integer, ProfileServiceConnections>();
+ private final Map<Integer, ProfileServiceConnections> mProfileServices =
+ new HashMap<Integer, ProfileServiceConnections>();
private final boolean mPermissionReviewRequired;
private final IBluetoothCallback mBluetoothCallback = new IBluetoothCallback.Stub() {
@Override
- public void onBluetoothStateChange(int prevState, int newState) throws RemoteException {
- Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_STATE_CHANGE,prevState,newState);
+ public void onBluetoothStateChange(int prevState, int newState) throws RemoteException {
+ Message msg =
+ mHandler.obtainMessage(MESSAGE_BLUETOOTH_STATE_CHANGE, prevState, newState);
mHandler.sendMessage(msg);
}
};
private final UserRestrictionsListener mUserRestrictionsListener =
new UserRestrictionsListener() {
- @Override
- public void onUserRestrictionsChanged(int userId, Bundle newRestrictions,
- Bundle prevRestrictions) {
-
- if (UserRestrictionsUtils.restrictionsChanged(prevRestrictions, newRestrictions,
- UserManager.DISALLOW_BLUETOOTH_SHARING)) {
- updateOppLauncherComponentState(userId, newRestrictions.getBoolean(
- UserManager.DISALLOW_BLUETOOTH_SHARING));
- }
-
- // DISALLOW_BLUETOOTH can only be set by DO or PO on the system user.
- if (userId == UserHandle.USER_SYSTEM &&
- UserRestrictionsUtils.restrictionsChanged(
- prevRestrictions, newRestrictions, UserManager.DISALLOW_BLUETOOTH)) {
- if (userId == UserHandle.USER_SYSTEM && newRestrictions.getBoolean(
- UserManager.DISALLOW_BLUETOOTH)) {
- updateOppLauncherComponentState(userId, true); // Sharing disallowed
- sendDisableMsg(REASON_DISALLOWED);
- } else {
- updateOppLauncherComponentState(userId, newRestrictions.getBoolean(
- UserManager.DISALLOW_BLUETOOTH_SHARING));
+ @Override
+ public void onUserRestrictionsChanged(int userId, Bundle newRestrictions,
+ Bundle prevRestrictions) {
+
+ if (UserRestrictionsUtils.restrictionsChanged(prevRestrictions, newRestrictions,
+ UserManager.DISALLOW_BLUETOOTH_SHARING)) {
+ updateOppLauncherComponentState(userId,
+ newRestrictions.getBoolean(UserManager.DISALLOW_BLUETOOTH_SHARING));
+ }
+
+ // DISALLOW_BLUETOOTH can only be set by DO or PO on the system user.
+ if (userId == UserHandle.USER_SYSTEM
+ && UserRestrictionsUtils.restrictionsChanged(prevRestrictions,
+ newRestrictions, UserManager.DISALLOW_BLUETOOTH)) {
+ if (userId == UserHandle.USER_SYSTEM && newRestrictions.getBoolean(
+ UserManager.DISALLOW_BLUETOOTH)) {
+ updateOppLauncherComponentState(userId, true); // Sharing disallowed
+ sendDisableMsg(REASON_DISALLOWED);
+ } else {
+ updateOppLauncherComponentState(userId, newRestrictions.getBoolean(
+ UserManager.DISALLOW_BLUETOOTH_SHARING));
+ }
+ }
}
- }
- }
- };
+ };
private final ContentObserver mAirplaneModeObserver = new ContentObserver(null) {
@Override
public void onChange(boolean unused) {
- synchronized(this) {
+ synchronized (this) {
if (isBluetoothPersistedStateOn()) {
if (isAirplaneModeOn()) {
persistBluetoothSetting(BLUETOOTH_ON_AIRPLANE);
@@ -278,8 +279,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
mBluetoothLock.readLock().unlock();
}
- Slog.d(TAG, "Airplane Mode change - current state: " +
- BluetoothAdapter.nameForState(st));
+ Slog.d(TAG,
+ "Airplane Mode change - current state: " + BluetoothAdapter.nameForState(
+ st));
if (isAirplaneModeOn()) {
// Clear registered LE apps to force shut-off
@@ -295,11 +297,11 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
mEnableExternal = false;
}
} catch (RemoteException e) {
- Slog.e(TAG,"Unable to call onBrEdrDown", e);
+ Slog.e(TAG, "Unable to call onBrEdrDown", e);
} finally {
mBluetoothLock.readLock().unlock();
}
- } else if (st == BluetoothAdapter.STATE_ON){
+ } else if (st == BluetoothAdapter.STATE_ON) {
sendDisableMsg(REASON_AIRPLANE_MODE);
}
} else if (mEnableExternal) {
@@ -315,35 +317,42 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
String action = intent.getAction();
if (BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED.equals(action)) {
String newName = intent.getStringExtra(BluetoothAdapter.EXTRA_LOCAL_NAME);
- if (DBG) Slog.d(TAG, "Bluetooth Adapter name changed to " + newName);
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth Adapter name changed to " + newName);
+ }
if (newName != null) {
storeNameAndAddress(newName, null);
}
} else if (BluetoothAdapter.ACTION_BLUETOOTH_ADDRESS_CHANGED.equals(action)) {
String newAddress = intent.getStringExtra(BluetoothAdapter.EXTRA_BLUETOOTH_ADDRESS);
if (newAddress != null) {
- if (DBG) Slog.d(TAG, "Bluetooth Adapter address changed to " + newAddress);
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth Adapter address changed to " + newAddress);
+ }
storeNameAndAddress(null, newAddress);
} else {
- if (DBG) Slog.e(TAG, "No Bluetooth Adapter address parameter found");
+ if (DBG) {
+ Slog.e(TAG, "No Bluetooth Adapter address parameter found");
+ }
}
} else if (Intent.ACTION_SETTING_RESTORED.equals(action)) {
final String name = intent.getStringExtra(Intent.EXTRA_SETTING_NAME);
if (Settings.Global.BLUETOOTH_ON.equals(name)) {
// The Bluetooth On state may be changed during system restore.
- final String prevValue = intent.getStringExtra(
- Intent.EXTRA_SETTING_PREVIOUS_VALUE);
- final String newValue = intent.getStringExtra(
- Intent.EXTRA_SETTING_NEW_VALUE);
+ final String prevValue =
+ intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE);
+ final String newValue = intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE);
- if (DBG) Slog.d(TAG, "ACTION_SETTING_RESTORED with BLUETOOTH_ON, prevValue=" +
- prevValue + ", newValue=" + newValue);
+ if (DBG) {
+ Slog.d(TAG,
+ "ACTION_SETTING_RESTORED with BLUETOOTH_ON, prevValue=" + prevValue
+ + ", newValue=" + newValue);
+ }
if ((newValue != null) && (prevValue != null) && !prevValue.equals(newValue)) {
Message msg = mHandler.obtainMessage(MESSAGE_RESTORE_USER_SETTING,
- newValue.equals("0") ?
- RESTORE_SETTING_TO_OFF :
- RESTORE_SETTING_TO_ON, 0);
+ newValue.equals("0") ? RESTORE_SETTING_TO_OFF
+ : RESTORE_SETTING_TO_ON, 0);
mHandler.sendMessage(msg);
}
}
@@ -356,8 +365,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
mContext = context;
- mPermissionReviewRequired = context.getResources().getBoolean(
- com.android.internal.R.bool.config_permissionReviewRequired);
+ mPermissionReviewRequired = context.getResources()
+ .getBoolean(com.android.internal.R.bool.config_permissionReviewRequired);
mActiveLogs = new LinkedList<ActiveLog>();
mCrashTimestamps = new LinkedList<Long>();
@@ -389,23 +398,26 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
loadStoredNameAndAddress();
if (isBluetoothPersistedStateOn()) {
- if (DBG) Slog.d(TAG, "Startup: Bluetooth persisted state is ON.");
+ if (DBG) {
+ Slog.d(TAG, "Startup: Bluetooth persisted state is ON.");
+ }
mEnableExternal = true;
}
- String airplaneModeRadios = Settings.Global.getString(mContentResolver,
- Settings.Global.AIRPLANE_MODE_RADIOS);
- if (airplaneModeRadios == null ||
- airplaneModeRadios.contains(Settings.Global.RADIO_BLUETOOTH)) {
+ String airplaneModeRadios =
+ Settings.Global.getString(mContentResolver, Settings.Global.AIRPLANE_MODE_RADIOS);
+ if (airplaneModeRadios == null || airplaneModeRadios.contains(
+ Settings.Global.RADIO_BLUETOOTH)) {
mContentResolver.registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON),
- true, mAirplaneModeObserver);
+ Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true,
+ mAirplaneModeObserver);
}
int systemUiUid = -1;
try {
- systemUiUid = mContext.getPackageManager().getPackageUidAsUser("com.android.systemui",
- PackageManager.MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM);
+ systemUiUid = mContext.getPackageManager()
+ .getPackageUidAsUser("com.android.systemui", PackageManager.MATCH_SYSTEM_ONLY,
+ UserHandle.USER_SYSTEM);
} catch (PackageManager.NameNotFoundException e) {
// Some platforms, such as wearables do not have a system ui.
Slog.w(TAG, "Unable to resolve SystemUI's UID.", e);
@@ -416,7 +428,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
/**
* Returns true if airplane mode is currently on
*/
- private final boolean isAirplaneModeOn() {
+ private boolean isAirplaneModeOn() {
return Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
}
@@ -424,31 +436,32 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
/**
* Returns true if the Bluetooth saved state is "on"
*/
- private final boolean isBluetoothPersistedStateOn() {
- int state = Settings.Global.getInt(mContentResolver,
- Settings.Global.BLUETOOTH_ON, -1);
- if (DBG) Slog.d(TAG, "Bluetooth persisted state: " + state);
+ private boolean isBluetoothPersistedStateOn() {
+ int state = Settings.Global.getInt(mContentResolver, Settings.Global.BLUETOOTH_ON, -1);
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth persisted state: " + state);
+ }
return state != BLUETOOTH_OFF;
}
/**
* Returns true if the Bluetooth saved state is BLUETOOTH_ON_BLUETOOTH
*/
- private final boolean isBluetoothPersistedStateOnBluetooth() {
- return Settings.Global.getInt(mContentResolver,
- Settings.Global.BLUETOOTH_ON, BLUETOOTH_ON_BLUETOOTH) == BLUETOOTH_ON_BLUETOOTH;
+ private boolean isBluetoothPersistedStateOnBluetooth() {
+ return Settings.Global.getInt(mContentResolver, Settings.Global.BLUETOOTH_ON,
+ BLUETOOTH_ON_BLUETOOTH) == BLUETOOTH_ON_BLUETOOTH;
}
/**
* Save the Bluetooth on/off state
*/
private void persistBluetoothSetting(int value) {
- if (DBG) Slog.d(TAG, "Persisting Bluetooth Setting: " + value);
+ if (DBG) {
+ Slog.d(TAG, "Persisting Bluetooth Setting: " + value);
+ }
// waive WRITE_SECURE_SETTINGS permission check
long callingIdentity = Binder.clearCallingIdentity();
- Settings.Global.putInt(mContext.getContentResolver(),
- Settings.Global.BLUETOOTH_ON,
- value);
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.BLUETOOTH_ON, value);
Binder.restoreCallingIdentity(callingIdentity);
}
@@ -458,7 +471,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
* @return
*/
private boolean isNameAndAddressSet() {
- return mName !=null && mAddress!= null && mName.length()>0 && mAddress.length()>0;
+ return mName != null && mAddress != null && mName.length() > 0 && mAddress.length() > 0;
}
/**
@@ -466,17 +479,24 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
* in the local cache
*/
private void loadStoredNameAndAddress() {
- if (DBG) Slog.d(TAG, "Loading stored name and address");
- if (mContext.getResources().getBoolean
- (com.android.internal.R.bool.config_bluetooth_address_validation) &&
- Settings.Secure.getInt(mContentResolver, SECURE_SETTINGS_BLUETOOTH_ADDR_VALID, 0) == 0) {
+ if (DBG) {
+ Slog.d(TAG, "Loading stored name and address");
+ }
+ if (mContext.getResources()
+ .getBoolean(com.android.internal.R.bool.config_bluetooth_address_validation)
+ && Settings.Secure.getInt(mContentResolver, SECURE_SETTINGS_BLUETOOTH_ADDR_VALID, 0)
+ == 0) {
// if the valid flag is not set, don't load the address and name
- if (DBG) Slog.d(TAG, "invalid bluetooth name and address stored");
+ if (DBG) {
+ Slog.d(TAG, "invalid bluetooth name and address stored");
+ }
return;
}
mName = Settings.Secure.getString(mContentResolver, SECURE_SETTINGS_BLUETOOTH_NAME);
mAddress = Settings.Secure.getString(mContentResolver, SECURE_SETTINGS_BLUETOOTH_ADDRESS);
- if (DBG) Slog.d(TAG, "Stored bluetooth Name=" + mName + ",Address=" + mAddress);
+ if (DBG) {
+ Slog.d(TAG, "Stored bluetooth Name=" + mName + ",Address=" + mAddress);
+ }
}
/**
@@ -489,15 +509,20 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
if (name != null) {
Settings.Secure.putString(mContentResolver, SECURE_SETTINGS_BLUETOOTH_NAME, name);
mName = name;
- if (DBG) Slog.d(TAG,"Stored Bluetooth name: " +
- Settings.Secure.getString(mContentResolver,SECURE_SETTINGS_BLUETOOTH_NAME));
+ if (DBG) {
+ Slog.d(TAG, "Stored Bluetooth name: " + Settings.Secure.getString(mContentResolver,
+ SECURE_SETTINGS_BLUETOOTH_NAME));
+ }
}
if (address != null) {
Settings.Secure.putString(mContentResolver, SECURE_SETTINGS_BLUETOOTH_ADDRESS, address);
- mAddress=address;
- if (DBG) Slog.d(TAG,"Stored Bluetoothaddress: " +
- Settings.Secure.getString(mContentResolver,SECURE_SETTINGS_BLUETOOTH_ADDRESS));
+ mAddress = address;
+ if (DBG) {
+ Slog.d(TAG,
+ "Stored Bluetoothaddress: " + Settings.Secure.getString(mContentResolver,
+ SECURE_SETTINGS_BLUETOOTH_ADDRESS));
+ }
}
if ((name != null) && (address != null)) {
@@ -505,7 +530,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
}
- public IBluetooth registerAdapter(IBluetoothManagerCallback callback){
+ public IBluetooth registerAdapter(IBluetoothManagerCallback callback) {
if (callback == null) {
Slog.w(TAG, "Callback is null in registerAdapter");
return null;
@@ -522,19 +547,17 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
Slog.w(TAG, "Callback is null in unregisterAdapter");
return;
}
- mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM,
- "Need BLUETOOTH permission");
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
Message msg = mHandler.obtainMessage(MESSAGE_UNREGISTER_ADAPTER);
msg.obj = callback;
mHandler.sendMessage(msg);
}
public void registerStateChangeCallback(IBluetoothStateChangeCallback callback) {
- mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM,
- "Need BLUETOOTH permission");
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
if (callback == null) {
- Slog.w(TAG, "registerStateChangeCallback: Callback is null!");
- return;
+ Slog.w(TAG, "registerStateChangeCallback: Callback is null!");
+ return;
}
Message msg = mHandler.obtainMessage(MESSAGE_REGISTER_STATE_CHANGE_CALLBACK);
msg.obj = callback;
@@ -542,11 +565,10 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
public void unregisterStateChangeCallback(IBluetoothStateChangeCallback callback) {
- mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM,
- "Need BLUETOOTH permission");
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
if (callback == null) {
- Slog.w(TAG, "unregisterStateChangeCallback: Callback is null!");
- return;
+ Slog.w(TAG, "unregisterStateChangeCallback: Callback is null!");
+ return;
}
Message msg = mHandler.obtainMessage(MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK);
msg.obj = callback;
@@ -554,15 +576,16 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
public boolean isEnabled() {
- if ((Binder.getCallingUid() != Process.SYSTEM_UID) &&
- (!checkIfCallerIsForegroundUser())) {
- Slog.w(TAG,"isEnabled(): not allowed for non-active and non system user");
+ if ((Binder.getCallingUid() != Process.SYSTEM_UID) && (!checkIfCallerIsForegroundUser())) {
+ Slog.w(TAG, "isEnabled(): not allowed for non-active and non system user");
return false;
}
try {
mBluetoothLock.readLock().lock();
- if (mBluetooth != null) return mBluetooth.isEnabled();
+ if (mBluetooth != null) {
+ return mBluetooth.isEnabled();
+ }
} catch (RemoteException e) {
Slog.e(TAG, "isEnabled()", e);
} finally {
@@ -572,15 +595,16 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
public int getState() {
- if ((Binder.getCallingUid() != Process.SYSTEM_UID) &&
- (!checkIfCallerIsForegroundUser())) {
+ if ((Binder.getCallingUid() != Process.SYSTEM_UID) && (!checkIfCallerIsForegroundUser())) {
Slog.w(TAG, "getState(): report OFF for non-active and non system user");
return BluetoothAdapter.STATE_OFF;
}
try {
mBluetoothLock.readLock().lock();
- if (mBluetooth != null) return mBluetooth.getState();
+ if (mBluetooth != null) {
+ return mBluetooth.getState();
+ }
} catch (RemoteException e) {
Slog.e(TAG, "getState()", e);
} finally {
@@ -592,26 +616,29 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
class ClientDeathRecipient implements IBinder.DeathRecipient {
private String mPackageName;
- public ClientDeathRecipient(String packageName) {
+ ClientDeathRecipient(String packageName) {
mPackageName = packageName;
}
public void binderDied() {
- if (DBG) Slog.d(TAG, "Binder is dead - unregister " + mPackageName);
+ if (DBG) {
+ Slog.d(TAG, "Binder is dead - unregister " + mPackageName);
+ }
if (isBleAppPresent()) {
- // Nothing to do, another app is here.
- return;
+ // Nothing to do, another app is here.
+ return;
+ }
+ if (DBG) {
+ Slog.d(TAG, "Disabling LE only mode after application crash");
}
- if (DBG) Slog.d(TAG, "Disabling LE only mode after application crash");
try {
mBluetoothLock.readLock().lock();
- if (mBluetooth != null &&
- mBluetooth.getState() == BluetoothAdapter.STATE_BLE_ON) {
+ if (mBluetooth != null && mBluetooth.getState() == BluetoothAdapter.STATE_BLE_ON) {
mEnable = false;
mBluetooth.onBrEdrDown();
}
} catch (RemoteException e) {
- Slog.e(TAG,"Unable to call onBrEdrDown", e);
+ Slog.e(TAG, "Unable to call onBrEdrDown", e);
} finally {
mBluetoothLock.readLock().unlock();
}
@@ -641,15 +668,17 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
@Override
public void onChange(boolean selfChange) {
if (isBleScanAlwaysAvailable()) {
- // Nothing to do
- return;
+ // Nothing to do
+ return;
}
// BLE scan is not available.
disableBleScanMode();
clearBleApps();
try {
mBluetoothLock.readLock().lock();
- if (mBluetooth != null) mBluetooth.onBrEdrDown();
+ if (mBluetooth != null) {
+ mBluetooth.onBrEdrDown();
+ }
} catch (RemoteException e) {
Slog.e(TAG, "error when disabling bluetooth", e);
} finally {
@@ -659,8 +688,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
};
mContentResolver.registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE),
- false, contentObserver);
+ Settings.Global.getUriFor(Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE), false,
+ contentObserver);
}
// Disable ble scan only mode.
@@ -668,7 +697,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
try {
mBluetoothLock.writeLock().lock();
if (mBluetooth != null && (mBluetooth.getState() != BluetoothAdapter.STATE_ON)) {
- if (DBG) Slog.d(TAG, "Reseting the mEnable flag for clean disable");
+ if (DBG) {
+ Slog.d(TAG, "Reseting the mEnable flag for clean disable");
+ }
mEnable = false;
}
} catch (RemoteException e) {
@@ -688,15 +719,21 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
throw new IllegalArgumentException("BLE app (" + packageName + ") already dead!");
}
mBleApps.put(token, deathRec);
- if (DBG) Slog.d(TAG, "Registered for death of " + packageName);
+ if (DBG) {
+ Slog.d(TAG, "Registered for death of " + packageName);
+ }
} else if (!enable && r != null) {
// Unregister death recipient as the app goes away.
token.unlinkToDeath(r, 0);
mBleApps.remove(token);
- if (DBG) Slog.d(TAG, "Unregistered for death of " + packageName);
+ if (DBG) {
+ Slog.d(TAG, "Unregistered for death of " + packageName);
+ }
}
int appCount = mBleApps.size();
- if (DBG) Slog.d(TAG, appCount + " registered Ble Apps");
+ if (DBG) {
+ Slog.d(TAG, appCount + " registered Ble Apps");
+ }
if (appCount == 0 && mEnable) {
disableBleScanMode();
}
@@ -713,7 +750,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
/** @hide */
public boolean isBleAppPresent() {
- if (DBG) Slog.d(TAG, "isBleAppPresent() count: " + mBleApps.size());
+ if (DBG) {
+ Slog.d(TAG, "isBleAppPresent() count: " + mBleApps.size());
+ }
return mBleApps.size() > 0;
}
@@ -721,17 +760,23 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
* Action taken when GattService is turned on
*/
private void onBluetoothGattServiceUp() {
- if (DBG) Slog.d(TAG,"BluetoothGatt Service is Up");
+ if (DBG) {
+ Slog.d(TAG, "BluetoothGatt Service is Up");
+ }
try {
mBluetoothLock.readLock().lock();
if (mBluetooth == null) {
- if (DBG) Slog.w(TAG, "onBluetoothServiceUp: mBluetooth is null!");
+ if (DBG) {
+ Slog.w(TAG, "onBluetoothServiceUp: mBluetooth is null!");
+ }
return;
}
int st = mBluetooth.getState();
if (st != BluetoothAdapter.STATE_BLE_ON) {
- if (DBG) Slog.v(TAG, "onBluetoothServiceUp: state isn't BLE_ON: " +
- BluetoothAdapter.nameForState(st));
+ if (DBG) {
+ Slog.v(TAG, "onBluetoothServiceUp: state isn't BLE_ON: "
+ + BluetoothAdapter.nameForState(st));
+ }
return;
}
if (isBluetoothPersistedStateOnBluetooth() || !isBleAppPresent()) {
@@ -740,7 +785,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH);
}
} catch (RemoteException e) {
- Slog.e(TAG,"Unable to call onServiceUp", e);
+ Slog.e(TAG, "Unable to call onServiceUp", e);
} finally {
mBluetoothLock.readLock().unlock();
}
@@ -751,7 +796,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
* and turn off all service and stack if no LE app needs it
*/
private void sendBrEdrDownCallback() {
- if (DBG) Slog.d(TAG,"Calling sendBrEdrDownCallback callbacks");
+ if (DBG) {
+ Slog.d(TAG, "Calling sendBrEdrDownCallback callbacks");
+ }
if (mBluetooth == null) {
Slog.w(TAG, "Bluetooth handle is null");
@@ -768,7 +815,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
} else {
try {
mBluetoothLock.readLock().lock();
- if (mBluetooth != null) mBluetooth.onBrEdrDown();
+ if (mBluetooth != null) {
+ mBluetooth.onBrEdrDown();
+ }
} catch (RemoteException e) {
Slog.e(TAG, "Call to onBrEdrDown() failed.", e);
} finally {
@@ -778,8 +827,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
- public boolean enableNoAutoConnect(String packageName)
- {
+ public boolean enableNoAutoConnect(String packageName) {
if (isBluetoothDisallowed()) {
if (DBG) {
Slog.d(TAG, "enableNoAutoConnect(): not enabling - bluetooth disallowed");
@@ -788,11 +836,11 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
- "Need BLUETOOTH ADMIN permission");
+ "Need BLUETOOTH ADMIN permission");
if (DBG) {
- Slog.d(TAG,"enableNoAutoConnect(): mBluetooth =" + mBluetooth +
- " mBinding = " + mBinding);
+ Slog.d(TAG, "enableNoAutoConnect(): mBluetooth =" + mBluetooth + " mBinding = "
+ + mBinding);
}
int callingAppId = UserHandle.getAppId(Binder.getCallingUid());
@@ -800,7 +848,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
throw new SecurityException("no permission to enable Bluetooth quietly");
}
- synchronized(mReceiver) {
+ synchronized (mReceiver) {
mQuietEnableExternal = true;
mEnableExternal = true;
sendEnableMsg(true, packageName);
@@ -814,7 +862,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
if (isBluetoothDisallowed()) {
if (DBG) {
- Slog.d(TAG,"enable(): not enabling - bluetooth disallowed");
+ Slog.d(TAG, "enable(): not enabling - bluetooth disallowed");
}
return false;
}
@@ -828,26 +876,26 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
"Need BLUETOOTH ADMIN permission");
- if (!isEnabled() && mPermissionReviewRequired
- && startConsentUiIfNeeded(packageName, callingUid,
- BluetoothAdapter.ACTION_REQUEST_ENABLE)) {
+ if (!isEnabled() && mPermissionReviewRequired && startConsentUiIfNeeded(packageName,
+ callingUid, BluetoothAdapter.ACTION_REQUEST_ENABLE)) {
return false;
}
}
if (DBG) {
- Slog.d(TAG,"enable(" + packageName + "): mBluetooth =" + mBluetooth +
- " mBinding = " + mBinding + " mState = " +
- BluetoothAdapter.nameForState(mState));
+ Slog.d(TAG, "enable(" + packageName + "): mBluetooth =" + mBluetooth + " mBinding = "
+ + mBinding + " mState = " + BluetoothAdapter.nameForState(mState));
}
- synchronized(mReceiver) {
+ synchronized (mReceiver) {
mQuietEnableExternal = false;
mEnableExternal = true;
// waive WRITE_SECURE_SETTINGS permission check
sendEnableMsg(false, packageName);
}
- if (DBG) Slog.d(TAG, "enable returning");
+ if (DBG) {
+ Slog.d(TAG, "enable returning");
+ }
return true;
}
@@ -864,19 +912,17 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
"Need BLUETOOTH ADMIN permission");
- if (isEnabled() && mPermissionReviewRequired
- && startConsentUiIfNeeded(packageName, callingUid,
- BluetoothAdapter.ACTION_REQUEST_DISABLE)) {
+ if (isEnabled() && mPermissionReviewRequired && startConsentUiIfNeeded(packageName,
+ callingUid, BluetoothAdapter.ACTION_REQUEST_DISABLE)) {
return false;
}
}
if (DBG) {
- Slog.d(TAG,"disable(): mBluetooth = " + mBluetooth +
- " mBinding = " + mBinding);
+ Slog.d(TAG, "disable(): mBluetooth = " + mBluetooth + " mBinding = " + mBinding);
}
- synchronized(mReceiver) {
+ synchronized (mReceiver) {
if (persist) {
persistBluetoothSetting(BLUETOOTH_OFF);
}
@@ -886,8 +932,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
return true;
}
- private boolean startConsentUiIfNeeded(String packageName,
- int callingUid, String intentAction) throws RemoteException {
+ private boolean startConsentUiIfNeeded(String packageName, int callingUid, String intentAction)
+ throws RemoteException {
try {
// Validate the package only if we are going to use it
ApplicationInfo applicationInfo = mContext.getPackageManager()
@@ -895,14 +941,13 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
UserHandle.getUserId(callingUid));
if (applicationInfo.uid != callingUid) {
- throw new SecurityException("Package " + callingUid
- + " not in uid " + callingUid);
+ throw new SecurityException("Package " + callingUid + " not in uid " + callingUid);
}
Intent intent = new Intent(intentAction);
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
try {
mContext.startActivity(intent);
} catch (ActivityNotFoundException e) {
@@ -918,13 +963,15 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
public void unbindAndFinish() {
if (DBG) {
- Slog.d(TAG,"unbindAndFinish(): " + mBluetooth +
- " mBinding = " + mBinding + " mUnbinding = " + mUnbinding);
+ Slog.d(TAG, "unbindAndFinish(): " + mBluetooth + " mBinding = " + mBinding
+ + " mUnbinding = " + mUnbinding);
}
try {
mBluetoothLock.writeLock().lock();
- if (mUnbinding) return;
+ if (mUnbinding) {
+ return;
+ }
mUnbinding = true;
mHandler.removeMessages(MESSAGE_BLUETOOTH_STATE_CHANGE);
mHandler.removeMessages(MESSAGE_BIND_PROFILE_SERVICE);
@@ -933,7 +980,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
try {
mBluetooth.unregisterCallback(mBluetoothCallback);
} catch (RemoteException re) {
- Slog.e(TAG, "Unable to unregister BluetoothCallback",re);
+ Slog.e(TAG, "Unable to unregister BluetoothCallback", re);
}
mBluetoothBinder = null;
mBluetooth = null;
@@ -959,8 +1006,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
IBluetoothProfileServiceConnection proxy) {
if (!mEnable) {
if (DBG) {
- Slog.d(TAG, "Trying to bind to profile: " + bluetoothProfile +
- ", while Bluetooth was disabled");
+ Slog.d(TAG, "Trying to bind to profile: " + bluetoothProfile
+ + ", while Bluetooth was disabled");
}
return false;
}
@@ -968,15 +1015,19 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
ProfileServiceConnections psc = mProfileServices.get(new Integer(bluetoothProfile));
if (psc == null) {
if (DBG) {
- Slog.d(TAG, "Creating new ProfileServiceConnections object for"
- + " profile: " + bluetoothProfile);
+ Slog.d(TAG, "Creating new ProfileServiceConnections object for" + " profile: "
+ + bluetoothProfile);
}
- if (bluetoothProfile != BluetoothProfile.HEADSET) return false;
+ if (bluetoothProfile != BluetoothProfile.HEADSET) {
+ return false;
+ }
Intent intent = new Intent(IBluetoothHeadset.class.getName());
psc = new ProfileServiceConnections(intent);
- if (!psc.bindService()) return false;
+ if (!psc.bindService()) {
+ return false;
+ }
mProfileServices.put(new Integer(bluetoothProfile), psc);
}
@@ -1022,7 +1073,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
* PHASE_SYSTEM_SERVICES_READY.
*/
public void handleOnBootPhase() {
- if (DBG) Slog.d(TAG, "Bluetooth boot completed");
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth boot completed");
+ }
UserManagerInternal userManagerInternal =
LocalServices.getService(UserManagerInternal.class);
userManagerInternal.addUserRestrictionsListener(mUserRestrictionsListener);
@@ -1031,10 +1084,14 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
return;
}
if (mEnableExternal && isBluetoothPersistedStateOnBluetooth()) {
- if (DBG) Slog.d(TAG, "Auto-enabling Bluetooth.");
+ if (DBG) {
+ Slog.d(TAG, "Auto-enabling Bluetooth.");
+ }
sendEnableMsg(mQuietEnableExternal, REASON_SYSTEM_BOOT);
} else if (!isNameAndAddressSet()) {
- if (DBG) Slog.d(TAG, "Getting adapter name and address");
+ if (DBG) {
+ Slog.d(TAG, "Getting adapter name and address");
+ }
Message getMsg = mHandler.obtainMessage(MESSAGE_GET_NAME_AND_ADDRESS);
mHandler.sendMessage(getMsg);
}
@@ -1044,7 +1101,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
* Called when switching to a different foreground user.
*/
public void handleOnSwitchUser(int userHandle) {
- if (DBG) Slog.d(TAG, "User " + userHandle + " switched");
+ if (DBG) {
+ Slog.d(TAG, "User " + userHandle + " switched");
+ }
mHandler.obtainMessage(MESSAGE_USER_SWITCHED, userHandle, 0).sendToTarget();
}
@@ -1052,7 +1111,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
* Called when user is unlocked.
*/
public void handleOnUnlockUser(int userHandle) {
- if (DBG) Slog.d(TAG, "User " + userHandle + " unlocked");
+ if (DBG) {
+ Slog.d(TAG, "User " + userHandle + " unlocked");
+ }
mHandler.obtainMessage(MESSAGE_USER_UNLOCKED, userHandle, 0).sendToTarget();
}
@@ -1060,10 +1121,10 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
* This class manages the clients connected to a given ProfileService
* and maintains the connection with that service.
*/
- final private class ProfileServiceConnections implements ServiceConnection,
- IBinder.DeathRecipient {
+ private final class ProfileServiceConnections
+ implements ServiceConnection, IBinder.DeathRecipient {
final RemoteCallbackList<IBluetoothProfileServiceConnection> mProxies =
- new RemoteCallbackList <IBluetoothProfileServiceConnection>();
+ new RemoteCallbackList<IBluetoothProfileServiceConnection>();
IBinder mService;
ComponentName mClassName;
Intent mIntent;
@@ -1076,8 +1137,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
private boolean bindService() {
- if (mIntent != null && mService == null &&
- doBind(mIntent, this, 0, UserHandle.CURRENT_OR_SELF)) {
+ if (mIntent != null && mService == null && doBind(mIntent, this, 0,
+ UserHandle.CURRENT_OR_SELF)) {
Message msg = mHandler.obtainMessage(MESSAGE_BIND_PROFILE_SERVICE);
msg.obj = this;
mHandler.sendMessageDelayed(msg, TIMEOUT_BIND_MS);
@@ -1090,7 +1151,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private void addProxy(IBluetoothProfileServiceConnection proxy) {
mProxies.register(proxy);
if (mService != null) {
- try{
+ try {
proxy.onServiceConnected(mClassName, mService);
} catch (RemoteException e) {
Slog.e(TAG, "Unable to connect to proxy", e);
@@ -1158,7 +1219,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
@Override
public void onServiceDisconnected(ComponentName className) {
- if (mService == null) return;
+ if (mService == null) {
+ return;
+ }
mService.unlinkToDeath(this, 0);
mService = null;
mClassName = null;
@@ -1187,8 +1250,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
@Override
public void binderDied() {
if (DBG) {
- Slog.w(TAG, "Profile service for profile: " + mClassName
- + " died.");
+ Slog.w(TAG, "Profile service for profile: " + mClassName + " died.");
}
onServiceDisconnected(mClassName);
// Trigger rebind
@@ -1201,12 +1263,15 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private void sendBluetoothStateCallback(boolean isUp) {
try {
int n = mStateChangeCallbacks.beginBroadcast();
- if (DBG) Slog.d(TAG,"Broadcasting onBluetoothStateChange("+isUp+") to " + n + " receivers.");
- for (int i=0; i <n;i++) {
+ if (DBG) {
+ Slog.d(TAG, "Broadcasting onBluetoothStateChange(" + isUp + ") to " + n
+ + " receivers.");
+ }
+ for (int i = 0; i < n; i++) {
try {
mStateChangeCallbacks.getBroadcastItem(i).onBluetoothStateChange(isUp);
} catch (RemoteException e) {
- Slog.e(TAG, "Unable to call onBluetoothStateChange() on callback #" + i , e);
+ Slog.e(TAG, "Unable to call onBluetoothStateChange() on callback #" + i, e);
}
}
} finally {
@@ -1220,11 +1285,11 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private void sendBluetoothServiceUpCallback() {
try {
int n = mCallbacks.beginBroadcast();
- Slog.d(TAG,"Broadcasting onBluetoothServiceUp() to " + n + " receivers.");
- for (int i=0; i <n;i++) {
+ Slog.d(TAG, "Broadcasting onBluetoothServiceUp() to " + n + " receivers.");
+ for (int i = 0; i < n; i++) {
try {
mCallbacks.getBroadcastItem(i).onBluetoothServiceUp(mBluetooth);
- } catch (RemoteException e) {
+ } catch (RemoteException e) {
Slog.e(TAG, "Unable to call onBluetoothServiceUp() on callback #" + i, e);
}
}
@@ -1232,17 +1297,18 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
mCallbacks.finishBroadcast();
}
}
+
/**
* Inform BluetoothAdapter instances that Adapter service is down
*/
private void sendBluetoothServiceDownCallback() {
try {
int n = mCallbacks.beginBroadcast();
- Slog.d(TAG,"Broadcasting onBluetoothServiceDown() to " + n + " receivers.");
- for (int i=0; i <n;i++) {
+ Slog.d(TAG, "Broadcasting onBluetoothServiceDown() to " + n + " receivers.");
+ for (int i = 0; i < n; i++) {
try {
mCallbacks.getBroadcastItem(i).onBluetoothServiceDown();
- } catch (RemoteException e) {
+ } catch (RemoteException e) {
Slog.e(TAG, "Unable to call onBluetoothServiceDown() on callback #" + i, e);
}
}
@@ -1252,12 +1318,10 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
public String getAddress() {
- mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM,
- "Need BLUETOOTH permission");
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- if ((Binder.getCallingUid() != Process.SYSTEM_UID) &&
- (!checkIfCallerIsForegroundUser())) {
- Slog.w(TAG,"getAddress(): not allowed for non-active and non system user");
+ if ((Binder.getCallingUid() != Process.SYSTEM_UID) && (!checkIfCallerIsForegroundUser())) {
+ Slog.w(TAG, "getAddress(): not allowed for non-active and non system user");
return null;
}
@@ -1268,9 +1332,13 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
try {
mBluetoothLock.readLock().lock();
- if (mBluetooth != null) return mBluetooth.getAddress();
+ if (mBluetooth != null) {
+ return mBluetooth.getAddress();
+ }
} catch (RemoteException e) {
- Slog.e(TAG, "getAddress(): Unable to retrieve address remotely. Returning cached address", e);
+ Slog.e(TAG,
+ "getAddress(): Unable to retrieve address remotely. Returning cached address",
+ e);
} finally {
mBluetoothLock.readLock().unlock();
}
@@ -1282,18 +1350,18 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
public String getName() {
- mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM,
- "Need BLUETOOTH permission");
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- if ((Binder.getCallingUid() != Process.SYSTEM_UID) &&
- (!checkIfCallerIsForegroundUser())) {
- Slog.w(TAG,"getName(): not allowed for non-active and non system user");
+ if ((Binder.getCallingUid() != Process.SYSTEM_UID) && (!checkIfCallerIsForegroundUser())) {
+ Slog.w(TAG, "getName(): not allowed for non-active and non system user");
return null;
}
try {
mBluetoothLock.readLock().lock();
- if (mBluetooth != null) return mBluetooth.getName();
+ if (mBluetooth != null) {
+ return mBluetooth.getName();
+ }
} catch (RemoteException e) {
Slog.e(TAG, "getName(): Unable to retrieve name remotely. Returning cached name", e);
} finally {
@@ -1309,7 +1377,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private class BluetoothServiceConnection implements ServiceConnection {
public void onServiceConnected(ComponentName componentName, IBinder service) {
String name = componentName.getClassName();
- if (DBG) Slog.d(TAG, "BluetoothServiceConnection: " + name);
+ if (DBG) {
+ Slog.d(TAG, "BluetoothServiceConnection: " + name);
+ }
Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_SERVICE_CONNECTED);
if (name.equals("com.android.bluetooth.btservice.AdapterService")) {
msg.arg1 = SERVICE_IBLUETOOTH;
@@ -1326,7 +1396,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
public void onServiceDisconnected(ComponentName componentName) {
// Called if we unexpectedly disconnect.
String name = componentName.getClassName();
- if (DBG) Slog.d(TAG, "BluetoothServiceConnection, disconnected: " + name);
+ if (DBG) {
+ Slog.d(TAG, "BluetoothServiceConnection, disconnected: " + name);
+ }
Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED);
if (name.equals("com.android.bluetooth.btservice.AdapterService")) {
msg.arg1 = SERVICE_IBLUETOOTH;
@@ -1345,7 +1417,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private class BluetoothHandler extends Handler {
boolean mGetNameAddressOnly = false;
- public BluetoothHandler(Looper looper) {
+ BluetoothHandler(Looper looper) {
super(looper);
}
@@ -1353,26 +1425,29 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_GET_NAME_AND_ADDRESS:
- if (DBG) Slog.d(TAG, "MESSAGE_GET_NAME_AND_ADDRESS");
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_GET_NAME_AND_ADDRESS");
+ }
try {
mBluetoothLock.writeLock().lock();
if ((mBluetooth == null) && (!mBinding)) {
- if (DBG) Slog.d(TAG, "Binding to service to get name and address");
+ if (DBG) {
+ Slog.d(TAG, "Binding to service to get name and address");
+ }
mGetNameAddressOnly = true;
Message timeoutMsg = mHandler.obtainMessage(MESSAGE_TIMEOUT_BIND);
mHandler.sendMessageDelayed(timeoutMsg, TIMEOUT_BIND_MS);
Intent i = new Intent(IBluetooth.class.getName());
if (!doBind(i, mConnection,
- Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
- UserHandle.CURRENT)) {
+ Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
+ UserHandle.CURRENT)) {
mHandler.removeMessages(MESSAGE_TIMEOUT_BIND);
} else {
mBinding = true;
}
} else if (mBluetooth != null) {
try {
- storeNameAndAddress(mBluetooth.getName(),
- mBluetooth.getAddress());
+ storeNameAndAddress(mBluetooth.getName(), mBluetooth.getAddress());
} catch (RemoteException re) {
Slog.e(TAG, "Unable to grab names", re);
}
@@ -1431,15 +1506,16 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
// on the order of (2 * SERVICE_RESTART_TIME_MS).
//
waitForOnOff(false, true);
- Message restartMsg = mHandler.obtainMessage(
- MESSAGE_RESTART_BLUETOOTH_SERVICE);
- mHandler.sendMessageDelayed(restartMsg,
- 2 * SERVICE_RESTART_TIME_MS);
+ Message restartMsg =
+ mHandler.obtainMessage(MESSAGE_RESTART_BLUETOOTH_SERVICE);
+ mHandler.sendMessageDelayed(restartMsg, 2 * SERVICE_RESTART_TIME_MS);
}
break;
case MESSAGE_DISABLE:
- if (DBG) Slog.d(TAG, "MESSAGE_DISABLE: mBluetooth = " + mBluetooth);
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_DISABLE: mBluetooth = " + mBluetooth);
+ }
mHandler.removeMessages(MESSAGE_RESTART_BLUETOOTH_SERVICE);
if (mEnable && mBluetooth != null) {
waitForOnOff(true, false);
@@ -1455,45 +1531,45 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
case MESSAGE_RESTORE_USER_SETTING:
try {
if ((msg.arg1 == RESTORE_SETTING_TO_OFF) && mEnable) {
- if (DBG) Slog.d(TAG, "Restore Bluetooth state to disabled");
+ if (DBG) {
+ Slog.d(TAG, "Restore Bluetooth state to disabled");
+ }
disable(REASON_RESTORE_USER_SETTING, true);
} else if ((msg.arg1 == RESTORE_SETTING_TO_ON) && !mEnable) {
- if (DBG) Slog.d(TAG, "Restore Bluetooth state to enabled");
+ if (DBG) {
+ Slog.d(TAG, "Restore Bluetooth state to enabled");
+ }
enable(REASON_RESTORE_USER_SETTING);
}
} catch (RemoteException e) {
- Slog.e(TAG,"Unable to change Bluetooth On setting", e);
+ Slog.e(TAG, "Unable to change Bluetooth On setting", e);
}
break;
- case MESSAGE_REGISTER_ADAPTER:
- {
+ case MESSAGE_REGISTER_ADAPTER: {
IBluetoothManagerCallback callback = (IBluetoothManagerCallback) msg.obj;
mCallbacks.register(callback);
break;
}
- case MESSAGE_UNREGISTER_ADAPTER:
- {
+ case MESSAGE_UNREGISTER_ADAPTER: {
IBluetoothManagerCallback callback = (IBluetoothManagerCallback) msg.obj;
mCallbacks.unregister(callback);
break;
}
- case MESSAGE_REGISTER_STATE_CHANGE_CALLBACK:
- {
- IBluetoothStateChangeCallback callback = (IBluetoothStateChangeCallback) msg.obj;
+ case MESSAGE_REGISTER_STATE_CHANGE_CALLBACK: {
+ IBluetoothStateChangeCallback callback =
+ (IBluetoothStateChangeCallback) msg.obj;
mStateChangeCallbacks.register(callback);
break;
}
- case MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK:
- {
- IBluetoothStateChangeCallback callback = (IBluetoothStateChangeCallback) msg.obj;
+ case MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK: {
+ IBluetoothStateChangeCallback callback =
+ (IBluetoothStateChangeCallback) msg.obj;
mStateChangeCallbacks.unregister(callback);
break;
}
- case MESSAGE_ADD_PROXY_DELAYED:
- {
- ProfileServiceConnections psc = mProfileServices.get(
- new Integer(msg.arg1));
+ case MESSAGE_ADD_PROXY_DELAYED: {
+ ProfileServiceConnections psc = mProfileServices.get(new Integer(msg.arg1));
if (psc == null) {
break;
}
@@ -1502,8 +1578,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
psc.addProxy(proxy);
break;
}
- case MESSAGE_BIND_PROFILE_SERVICE:
- {
+ case MESSAGE_BIND_PROFILE_SERVICE: {
ProfileServiceConnections psc = (ProfileServiceConnections) msg.obj;
removeMessages(MESSAGE_BIND_PROFILE_SERVICE, msg.obj);
if (psc == null) {
@@ -1512,16 +1587,17 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
psc.bindService();
break;
}
- case MESSAGE_BLUETOOTH_SERVICE_CONNECTED:
- {
- if (DBG) Slog.d(TAG,"MESSAGE_BLUETOOTH_SERVICE_CONNECTED: " + msg.arg1);
+ case MESSAGE_BLUETOOTH_SERVICE_CONNECTED: {
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_BLUETOOTH_SERVICE_CONNECTED: " + msg.arg1);
+ }
IBinder service = (IBinder) msg.obj;
try {
mBluetoothLock.writeLock().lock();
if (msg.arg1 == SERVICE_IBLUETOOTHGATT) {
- mBluetoothGatt = IBluetoothGatt.Stub
- .asInterface(Binder.allowBlocking(service));
+ mBluetoothGatt =
+ IBluetoothGatt.Stub.asInterface(Binder.allowBlocking(service));
onBluetoothGattServiceUp();
break;
} // else must be SERVICE_IBLUETOOTH
@@ -1536,31 +1612,33 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
if (!isNameAndAddressSet()) {
Message getMsg = mHandler.obtainMessage(MESSAGE_GET_NAME_AND_ADDRESS);
mHandler.sendMessage(getMsg);
- if (mGetNameAddressOnly) return;
+ if (mGetNameAddressOnly) {
+ return;
+ }
}
//Register callback object
try {
mBluetooth.registerCallback(mBluetoothCallback);
} catch (RemoteException re) {
- Slog.e(TAG, "Unable to register BluetoothCallback",re);
+ Slog.e(TAG, "Unable to register BluetoothCallback", re);
}
//Inform BluetoothAdapter instances that service is up
sendBluetoothServiceUpCallback();
//Do enable request
try {
- if (mQuietEnable == false) {
+ if (!mQuietEnable) {
if (!mBluetooth.enable()) {
- Slog.e(TAG,"IBluetooth.enable() returned false");
+ Slog.e(TAG, "IBluetooth.enable() returned false");
}
} else {
if (!mBluetooth.enableNoAutoConnect()) {
- Slog.e(TAG,"IBluetooth.enableNoAutoConnect() returned false");
+ Slog.e(TAG, "IBluetooth.enableNoAutoConnect() returned false");
}
}
} catch (RemoteException e) {
- Slog.e(TAG,"Unable to call enable()",e);
+ Slog.e(TAG, "Unable to call enable()", e);
}
} finally {
mBluetoothLock.writeLock().unlock();
@@ -1573,43 +1651,42 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
break;
}
- case MESSAGE_BLUETOOTH_STATE_CHANGE:
- {
+ case MESSAGE_BLUETOOTH_STATE_CHANGE: {
int prevState = msg.arg1;
int newState = msg.arg2;
if (DBG) {
- Slog.d(TAG, "MESSAGE_BLUETOOTH_STATE_CHANGE: " + BluetoothAdapter.nameForState(prevState) + " > " +
- BluetoothAdapter.nameForState(newState));
+ Slog.d(TAG,
+ "MESSAGE_BLUETOOTH_STATE_CHANGE: " + BluetoothAdapter.nameForState(
+ prevState) + " > " + BluetoothAdapter.nameForState(
+ newState));
}
mState = newState;
bluetoothStateChangeHandler(prevState, newState);
// handle error state transition case from TURNING_ON to OFF
// unbind and rebind bluetooth service and enable bluetooth
- if ((prevState == BluetoothAdapter.STATE_BLE_TURNING_ON) &&
- (newState == BluetoothAdapter.STATE_OFF) &&
- (mBluetooth != null) && mEnable) {
+ if ((prevState == BluetoothAdapter.STATE_BLE_TURNING_ON) && (newState
+ == BluetoothAdapter.STATE_OFF) && (mBluetooth != null) && mEnable) {
recoverBluetoothServiceFromError(false);
}
- if ((prevState == BluetoothAdapter.STATE_TURNING_ON) &&
- (newState == BluetoothAdapter.STATE_BLE_ON) &&
- (mBluetooth != null) && mEnable) {
+ if ((prevState == BluetoothAdapter.STATE_TURNING_ON) && (newState
+ == BluetoothAdapter.STATE_BLE_ON) && (mBluetooth != null) && mEnable) {
recoverBluetoothServiceFromError(true);
}
// If we tried to enable BT while BT was in the process of shutting down,
// wait for the BT process to fully tear down and then force a restart
// here. This is a bit of a hack (b/29363429).
- if ((prevState == BluetoothAdapter.STATE_BLE_TURNING_OFF) &&
- (newState == BluetoothAdapter.STATE_OFF)) {
+ if ((prevState == BluetoothAdapter.STATE_BLE_TURNING_OFF) && (newState
+ == BluetoothAdapter.STATE_OFF)) {
if (mEnable) {
Slog.d(TAG, "Entering STATE_OFF but mEnabled is true; restarting.");
waitForOnOff(false, true);
- Message restartMsg = mHandler.obtainMessage(
- MESSAGE_RESTART_BLUETOOTH_SERVICE);
+ Message restartMsg =
+ mHandler.obtainMessage(MESSAGE_RESTART_BLUETOOTH_SERVICE);
mHandler.sendMessageDelayed(restartMsg, 2 * SERVICE_RESTART_TIME_MS);
}
}
- if (newState == BluetoothAdapter.STATE_ON ||
- newState == BluetoothAdapter.STATE_BLE_ON) {
+ if (newState == BluetoothAdapter.STATE_ON
+ || newState == BluetoothAdapter.STATE_BLE_ON) {
// bluetooth is working, reset the counter
if (mErrorRecoveryRetryCounter != 0) {
Slog.w(TAG, "bluetooth is recovered from error");
@@ -1618,14 +1695,15 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
break;
}
- case MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED:
- {
+ case MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED: {
Slog.e(TAG, "MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED(" + msg.arg1 + ")");
try {
mBluetoothLock.writeLock().lock();
if (msg.arg1 == SERVICE_IBLUETOOTH) {
// if service is unbinded already, do nothing and return
- if (mBluetooth == null) break;
+ if (mBluetooth == null) {
+ break;
+ }
mBluetooth = null;
} else if (msg.arg1 == SERVICE_IBLUETOOTHGATT) {
mBluetoothGatt = null;
@@ -1644,33 +1722,31 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
if (mEnable) {
mEnable = false;
// Send a Bluetooth Restart message
- Message restartMsg = mHandler.obtainMessage(
- MESSAGE_RESTART_BLUETOOTH_SERVICE);
- mHandler.sendMessageDelayed(restartMsg,
- SERVICE_RESTART_TIME_MS);
+ Message restartMsg =
+ mHandler.obtainMessage(MESSAGE_RESTART_BLUETOOTH_SERVICE);
+ mHandler.sendMessageDelayed(restartMsg, SERVICE_RESTART_TIME_MS);
}
sendBluetoothServiceDownCallback();
// Send BT state broadcast to update
// the BT icon correctly
- if ((mState == BluetoothAdapter.STATE_TURNING_ON) ||
- (mState == BluetoothAdapter.STATE_ON)) {
+ if ((mState == BluetoothAdapter.STATE_TURNING_ON) || (mState
+ == BluetoothAdapter.STATE_ON)) {
bluetoothStateChangeHandler(BluetoothAdapter.STATE_ON,
- BluetoothAdapter.STATE_TURNING_OFF);
+ BluetoothAdapter.STATE_TURNING_OFF);
mState = BluetoothAdapter.STATE_TURNING_OFF;
}
if (mState == BluetoothAdapter.STATE_TURNING_OFF) {
bluetoothStateChangeHandler(BluetoothAdapter.STATE_TURNING_OFF,
- BluetoothAdapter.STATE_OFF);
+ BluetoothAdapter.STATE_OFF);
}
mHandler.removeMessages(MESSAGE_BLUETOOTH_STATE_CHANGE);
mState = BluetoothAdapter.STATE_OFF;
break;
}
- case MESSAGE_RESTART_BLUETOOTH_SERVICE:
- {
+ case MESSAGE_RESTART_BLUETOOTH_SERVICE: {
Slog.d(TAG, "MESSAGE_RESTART_BLUETOOTH_SERVICE");
/* Enable without persisting the setting as
it doesnt change when IBluetooth
@@ -1687,8 +1763,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
mBluetoothLock.writeLock().unlock();
break;
}
- case MESSAGE_TIMEOUT_UNBIND:
- {
+ case MESSAGE_TIMEOUT_UNBIND: {
Slog.e(TAG, "MESSAGE_TIMEOUT_UNBIND");
mBluetoothLock.writeLock().lock();
mUnbinding = false;
@@ -1697,7 +1772,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
case MESSAGE_USER_SWITCHED: {
- if (DBG) Slog.d(TAG, "MESSAGE_USER_SWITCHED");
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_USER_SWITCHED");
+ }
mHandler.removeMessages(MESSAGE_USER_SWITCHED);
/* disable and enable BT when detect a user switch */
@@ -1735,12 +1812,12 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
handleDisable();
// Pbap service need receive STATE_TURNING_OFF intent to close
bluetoothStateChangeHandler(BluetoothAdapter.STATE_ON,
- BluetoothAdapter.STATE_TURNING_OFF);
+ BluetoothAdapter.STATE_TURNING_OFF);
boolean didDisableTimeout = !waitForOnOff(false, true);
bluetoothStateChangeHandler(BluetoothAdapter.STATE_TURNING_OFF,
- BluetoothAdapter.STATE_OFF);
+ BluetoothAdapter.STATE_OFF);
sendBluetoothServiceDownCallback();
try {
@@ -1785,14 +1862,18 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
break;
}
case MESSAGE_USER_UNLOCKED: {
- if (DBG) Slog.d(TAG, "MESSAGE_USER_UNLOCKED");
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_USER_UNLOCKED");
+ }
mHandler.removeMessages(MESSAGE_USER_SWITCHED);
if (mEnable && !mBinding && (mBluetooth == null)) {
// We should be connected, but we gave up for some
// reason; maybe the Bluetooth service wasn't encryption
// aware, so try binding again.
- if (DBG) Slog.d(TAG, "Enabled but not bound; retrying after unlock");
+ if (DBG) {
+ Slog.d(TAG, "Enabled but not bound; retrying after unlock");
+ }
handleEnable(mQuietEnable);
}
}
@@ -1807,10 +1888,10 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
mBluetoothLock.writeLock().lock();
if ((mBluetooth == null) && (!mBinding)) {
//Start bind timeout and bind
- Message timeoutMsg=mHandler.obtainMessage(MESSAGE_TIMEOUT_BIND);
- mHandler.sendMessageDelayed(timeoutMsg,TIMEOUT_BIND_MS);
+ Message timeoutMsg = mHandler.obtainMessage(MESSAGE_TIMEOUT_BIND);
+ mHandler.sendMessageDelayed(timeoutMsg, TIMEOUT_BIND_MS);
Intent i = new Intent(IBluetooth.class.getName());
- if (!doBind(i, mConnection,Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
+ if (!doBind(i, mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
UserHandle.CURRENT)) {
mHandler.removeMessages(MESSAGE_TIMEOUT_BIND);
} else {
@@ -1820,17 +1901,16 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
//Enable bluetooth
try {
if (!mQuietEnable) {
- if(!mBluetooth.enable()) {
- Slog.e(TAG,"IBluetooth.enable() returned false");
+ if (!mBluetooth.enable()) {
+ Slog.e(TAG, "IBluetooth.enable() returned false");
}
- }
- else {
- if(!mBluetooth.enableNoAutoConnect()) {
- Slog.e(TAG,"IBluetooth.enableNoAutoConnect() returned false");
+ } else {
+ if (!mBluetooth.enableNoAutoConnect()) {
+ Slog.e(TAG, "IBluetooth.enableNoAutoConnect() returned false");
}
}
} catch (RemoteException e) {
- Slog.e(TAG,"Unable to call enable()",e);
+ Slog.e(TAG, "Unable to call enable()", e);
}
}
} finally {
@@ -1852,13 +1932,15 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
try {
mBluetoothLock.readLock().lock();
if (mBluetooth != null) {
- if (DBG) Slog.d(TAG,"Sending off request.");
+ if (DBG) {
+ Slog.d(TAG, "Sending off request.");
+ }
if (!mBluetooth.disable()) {
- Slog.e(TAG,"IBluetooth.disable() returned false");
+ Slog.e(TAG, "IBluetooth.disable() returned false");
}
}
} catch (RemoteException e) {
- Slog.e(TAG,"Unable to call disable()",e);
+ Slog.e(TAG, "Unable to call disable()", e);
} finally {
mBluetoothLock.readLock().unlock();
}
@@ -1876,15 +1958,12 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
boolean valid = false;
try {
foregroundUser = ActivityManager.getCurrentUser();
- valid = (callingUser == foregroundUser) ||
- parentUser == foregroundUser ||
- callingAppId == Process.NFC_UID ||
- callingAppId == mSystemUiUid;
+ valid = (callingUser == foregroundUser) || parentUser == foregroundUser
+ || callingAppId == Process.NFC_UID || callingAppId == mSystemUiUid;
if (DBG && !valid) {
- Slog.d(TAG, "checkIfCallerIsForegroundUser: valid=" + valid
- + " callingUser=" + callingUser
- + " parentUser=" + parentUser
- + " foregroundUser=" + foregroundUser);
+ Slog.d(TAG, "checkIfCallerIsForegroundUser: valid=" + valid + " callingUser="
+ + callingUser + " parentUser=" + parentUser + " foregroundUser="
+ + foregroundUser);
}
} finally {
Binder.restoreCallingIdentity(callingIdentity);
@@ -1893,8 +1972,11 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
private void sendBleStateChanged(int prevState, int newState) {
- if (DBG) Slog.d(TAG,"Sending BLE State Change: " + BluetoothAdapter.nameForState(prevState) +
- " > " + BluetoothAdapter.nameForState(newState));
+ if (DBG) {
+ Slog.d(TAG,
+ "Sending BLE State Change: " + BluetoothAdapter.nameForState(prevState) + " > "
+ + BluetoothAdapter.nameForState(newState));
+ }
// Send broadcast message to everyone else
Intent intent = new Intent(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prevState);
@@ -1909,14 +1991,15 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
return;
}
// Notify all proxy objects first of adapter state change
- if (newState == BluetoothAdapter.STATE_BLE_ON ||
- newState == BluetoothAdapter.STATE_OFF) {
+ if (newState == BluetoothAdapter.STATE_BLE_ON || newState == BluetoothAdapter.STATE_OFF) {
boolean intermediate_off = (prevState == BluetoothAdapter.STATE_TURNING_OFF
- && newState == BluetoothAdapter.STATE_BLE_ON);
+ && newState == BluetoothAdapter.STATE_BLE_ON);
if (newState == BluetoothAdapter.STATE_OFF) {
// If Bluetooth is off, send service down event to proxy objects, and unbind
- if (DBG) Slog.d(TAG, "Bluetooth is complete send Service Down");
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth is complete send Service Down");
+ }
sendBluetoothServiceDownCallback();
unbindAndFinish();
sendBleStateChanged(prevState, newState);
@@ -1925,16 +2008,23 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
} else if (!intermediate_off) {
// connect to GattService
- if (DBG) Slog.d(TAG, "Bluetooth is in LE only mode");
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth is in LE only mode");
+ }
if (mBluetoothGatt != null) {
- if (DBG) Slog.d(TAG, "Calling BluetoothGattServiceUp");
+ if (DBG) {
+ Slog.d(TAG, "Calling BluetoothGattServiceUp");
+ }
onBluetoothGattServiceUp();
} else {
- if (DBG) Slog.d(TAG, "Binding Bluetooth GATT service");
- if (mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_BLUETOOTH_LE)) {
+ if (DBG) {
+ Slog.d(TAG, "Binding Bluetooth GATT service");
+ }
+ if (mContext.getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Intent i = new Intent(IBluetoothGatt.class.getName());
- doBind(i, mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.CURRENT);
+ doBind(i, mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
+ UserHandle.CURRENT);
}
}
sendBleStateChanged(prevState, newState);
@@ -1942,7 +2032,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
isStandardBroadcast = false;
} else if (intermediate_off) {
- if (DBG) Slog.d(TAG, "Intermediate off, back to LE only mode");
+ if (DBG) {
+ Slog.d(TAG, "Intermediate off, back to LE only mode");
+ }
// For LE only mode, broadcast as is
sendBleStateChanged(prevState, newState);
sendBluetoothStateCallback(false); // BT is OFF for general users
@@ -1955,13 +2047,13 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
sendBluetoothStateCallback(isUp);
sendBleStateChanged(prevState, newState);
- } else if (newState == BluetoothAdapter.STATE_BLE_TURNING_ON ||
- newState == BluetoothAdapter.STATE_BLE_TURNING_OFF ) {
+ } else if (newState == BluetoothAdapter.STATE_BLE_TURNING_ON
+ || newState == BluetoothAdapter.STATE_BLE_TURNING_OFF) {
sendBleStateChanged(prevState, newState);
isStandardBroadcast = false;
- } else if (newState == BluetoothAdapter.STATE_TURNING_ON ||
- newState == BluetoothAdapter.STATE_TURNING_OFF) {
+ } else if (newState == BluetoothAdapter.STATE_TURNING_ON
+ || newState == BluetoothAdapter.STATE_TURNING_OFF) {
sendBleStateChanged(prevState, newState);
}
@@ -1988,13 +2080,21 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
while (i < 10) {
try {
mBluetoothLock.readLock().lock();
- if (mBluetooth == null) break;
+ if (mBluetooth == null) {
+ break;
+ }
if (on) {
- if (mBluetooth.getState() == BluetoothAdapter.STATE_ON) return true;
+ if (mBluetooth.getState() == BluetoothAdapter.STATE_ON) {
+ return true;
+ }
} else if (off) {
- if (mBluetooth.getState() == BluetoothAdapter.STATE_OFF) return true;
+ if (mBluetooth.getState() == BluetoothAdapter.STATE_OFF) {
+ return true;
+ }
} else {
- if (mBluetooth.getState() != BluetoothAdapter.STATE_ON) return true;
+ if (mBluetooth.getState() != BluetoothAdapter.STATE_ON) {
+ return true;
+ }
}
} catch (RemoteException e) {
Slog.e(TAG, "getState()", e);
@@ -2009,7 +2109,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
i++;
}
- Slog.e(TAG,"waitForOnOff time out");
+ Slog.e(TAG, "waitForOnOff time out");
return false;
}
@@ -2019,8 +2119,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
private void sendEnableMsg(boolean quietMode, String packageName) {
- mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_ENABLE,
- quietMode ? 1 : 0, 0));
+ mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_ENABLE, quietMode ? 1 : 0, 0));
addActiveLog(packageName, true);
mLastEnabledTime = SystemClock.elapsedRealtime();
}
@@ -2035,15 +2134,17 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
private void addCrashLog() {
- synchronized (mCrashTimestamps) {
- if (mCrashTimestamps.size() == CRASH_LOG_MAX_SIZE) mCrashTimestamps.removeFirst();
- mCrashTimestamps.add(System.currentTimeMillis());
- mCrashes++;
- }
+ synchronized (mCrashTimestamps) {
+ if (mCrashTimestamps.size() == CRASH_LOG_MAX_SIZE) {
+ mCrashTimestamps.removeFirst();
+ }
+ mCrashTimestamps.add(System.currentTimeMillis());
+ mCrashes++;
+ }
}
private void recoverBluetoothServiceFromError(boolean clearBle) {
- Slog.e(TAG,"recoverBluetoothServiceFromError");
+ Slog.e(TAG, "recoverBluetoothServiceFromError");
try {
mBluetoothLock.readLock().lock();
if (mBluetooth != null) {
@@ -2082,15 +2183,14 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
mState = BluetoothAdapter.STATE_OFF;
if (clearBle) {
- clearBleApps();
+ clearBleApps();
}
mEnable = false;
if (mErrorRecoveryRetryCounter++ < MAX_ERROR_RESTART_RETRIES) {
// Send a Bluetooth Restart message to reenable bluetooth
- Message restartMsg = mHandler.obtainMessage(
- MESSAGE_RESTART_BLUETOOTH_SERVICE);
+ Message restartMsg = mHandler.obtainMessage(MESSAGE_RESTART_BLUETOOTH_SERVICE);
mHandler.sendMessageDelayed(restartMsg, ERROR_RESTART_TIME_MS);
} else {
// todo: notify user to power down and power up phone to make bluetooth work.
@@ -2118,9 +2218,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
private void updateOppLauncherComponentState(int userId, boolean bluetoothSharingDisallowed) {
final ComponentName oppLauncherComponent = new ComponentName("com.android.bluetooth",
"com.android.bluetooth.opp.BluetoothOppLauncherActivity");
- final int newState = bluetoothSharingDisallowed
- ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
- : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
+ final int newState =
+ bluetoothSharingDisallowed ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
try {
final IPackageManager imp = AppGlobals.getPackageManager();
imp.setComponentEnabledSetting(oppLauncherComponent, newState,
@@ -2132,7 +2232,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
- if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) {
+ return;
+ }
String errorMsg = null;
boolean protoOut = (args.length > 0) && args[0].startsWith("--proto");
@@ -2145,11 +2247,10 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
writer.println(" name: " + mName);
if (mEnable) {
long onDuration = SystemClock.elapsedRealtime() - mLastEnabledTime;
- String onDurationString = String.format("%02d:%02d:%02d.%03d",
- (int)(onDuration / (1000 * 60 * 60)),
- (int)((onDuration / (1000 * 60)) % 60),
- (int)((onDuration / 1000) % 60),
- (int)(onDuration % 1000));
+ String onDurationString = String.format(Locale.US, "%02d:%02d:%02d.%03d",
+ (int) (onDuration / (1000 * 60 * 60)),
+ (int) ((onDuration / (1000 * 60)) % 60), (int) ((onDuration / 1000) % 60),
+ (int) (onDuration % 1000));
writer.println(" time since enabled: " + onDurationString);
}
@@ -2162,14 +2263,17 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
}
- writer.println("\nBluetooth crashed " + mCrashes + " time" + (mCrashes == 1 ? "" : "s"));
- if (mCrashes == CRASH_LOG_MAX_SIZE) writer.println("(last " + CRASH_LOG_MAX_SIZE + ")");
+ writer.println(
+ "\nBluetooth crashed " + mCrashes + " time" + (mCrashes == 1 ? "" : "s"));
+ if (mCrashes == CRASH_LOG_MAX_SIZE) {
+ writer.println("(last " + CRASH_LOG_MAX_SIZE + ")");
+ }
for (Long time : mCrashTimestamps) {
- writer.println(" " + timeToLog(time.longValue()));
+ writer.println(" " + timeToLog(time));
}
- writer.println("\n" + mBleApps.size() + " BLE app" +
- (mBleApps.size() == 1 ? "" : "s") + "registered");
+ writer.println("\n" + mBleApps.size() + " BLE app" + (mBleApps.size() == 1 ? "" : "s")
+ + "registered");
for (ClientDeathRecipient app : mBleApps.values()) {
writer.println(" " + app.getPackageName());
}
@@ -2194,7 +2298,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
}
if (errorMsg != null) {
// Silently return if we are extracting metrics in Protobuf format
- if (protoOut) return;
+ if (protoOut) {
+ return;
+ }
writer.println(errorMsg);
}
}
diff --git a/com/android/server/BootReceiver.java b/com/android/server/BootReceiver.java
index 43544862..8848e393 100644
--- a/com/android/server/BootReceiver.java
+++ b/com/android/server/BootReceiver.java
@@ -33,6 +33,7 @@ import android.os.storage.StorageManager;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.AtomicFile;
+import android.util.EventLog;
import android.util.Slog;
import android.util.Xml;
@@ -40,6 +41,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;
+import com.android.server.DropboxLogTags;
import java.io.File;
import java.io.FileInputStream;
@@ -297,6 +299,7 @@ public class BootReceiver extends BroadcastReceiver {
Slog.i(TAG, "Copying " + filename + " to DropBox (" + tag + ")");
db.addText(tag, headers + FileUtils.readTextFile(file, maxSize, "[[TRUNCATED]]\n") +
footers);
+ EventLog.writeEvent(DropboxLogTags.DROPBOX_FILE_COPY, filename, maxSize, tag);
}
private static void addAuditErrorsToDropBox(DropBoxManager db,
diff --git a/com/android/server/ConnectivityService.java b/com/android/server/ConnectivityService.java
index bccae062..86b01646 100644
--- a/com/android/server/ConnectivityService.java
+++ b/com/android/server/ConnectivityService.java
@@ -972,7 +972,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
getNetworkTypeName(networkType), "");
info.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, null, null);
info.setIsAvailable(true);
- state = new NetworkState(info, new LinkProperties(), new NetworkCapabilities(),
+ final NetworkCapabilities capabilities = new NetworkCapabilities();
+ capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING,
+ !info.isRoaming());
+ state = new NetworkState(info, new LinkProperties(), capabilities,
null, null, null);
}
filterNetworkStateForUid(state, uid, ignoreBlocked);
diff --git a/com/android/server/DeviceIdleController.java b/com/android/server/DeviceIdleController.java
index d7aeb8ce..6f697c46 100644
--- a/com/android/server/DeviceIdleController.java
+++ b/com/android/server/DeviceIdleController.java
@@ -76,6 +76,7 @@ import android.util.SparseBooleanArray;
import android.util.TimeUtils;
import android.util.Xml;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IBatteryStats;
import com.android.internal.os.AtomicFile;
import com.android.internal.os.BackgroundThread;
@@ -1754,6 +1755,27 @@ public class DeviceIdleController extends SystemService
}
}
+ void removePowerSaveTempWhitelistAppChecked(String packageName, int userId)
+ throws RemoteException {
+ getContext().enforceCallingPermission(
+ Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST,
+ "No permission to change device idle whitelist");
+ final int callingUid = Binder.getCallingUid();
+ userId = ActivityManager.getService().handleIncomingUser(
+ Binder.getCallingPid(),
+ callingUid,
+ userId,
+ /*allowAll=*/ false,
+ /*requireFull=*/ false,
+ "removePowerSaveTempWhitelistApp", null);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ removePowerSaveTempWhitelistAppInternal(packageName, userId);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
/**
* Adds an app to the temporary whitelist and resets the endTime for granting the
* app an exemption to access network and acquire wakelocks.
@@ -1819,6 +1841,32 @@ public class DeviceIdleController extends SystemService
}
}
+ /**
+ * Removes an app from the temporary whitelist and notifies the observers.
+ */
+ private void removePowerSaveTempWhitelistAppInternal(String packageName, int userId) {
+ try {
+ final int uid = getContext().getPackageManager().getPackageUidAsUser(
+ packageName, userId);
+ final int appId = UserHandle.getAppId(uid);
+ removePowerSaveTempWhitelistAppDirectInternal(appId);
+ } catch (NameNotFoundException e) {
+ }
+ }
+
+ private void removePowerSaveTempWhitelistAppDirectInternal(int appId) {
+ synchronized (this) {
+ final int idx = mTempWhitelistAppIdEndTimes.indexOfKey(appId);
+ if (idx < 0) {
+ // Nothing else to do
+ return;
+ }
+ final String reason = mTempWhitelistAppIdEndTimes.valueAt(idx).second;
+ mTempWhitelistAppIdEndTimes.removeAt(idx);
+ onAppRemovedFromTempWhitelistLocked(appId, reason);
+ }
+ }
+
private void postTempActiveTimeoutMessage(int uid, long delay) {
if (DEBUG) {
Slog.d(TAG, "postTempActiveTimeoutMessage: uid=" + uid + ", delay=" + delay);
@@ -1840,18 +1888,7 @@ public class DeviceIdleController extends SystemService
}
if (timeNow >= entry.first.value) {
mTempWhitelistAppIdEndTimes.delete(uid);
- if (DEBUG) {
- Slog.d(TAG, "Removing UID " + uid + " from temp whitelist");
- }
- updateTempWhitelistAppIdsLocked(uid, false);
- mHandler.obtainMessage(MSG_REPORT_TEMP_APP_WHITELIST_CHANGED, uid, 0)
- .sendToTarget();
- reportTempWhitelistChangedLocked();
- try {
- mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_TEMP_WHITELIST_FINISH,
- entry.second, uid);
- } catch (RemoteException e) {
- }
+ onAppRemovedFromTempWhitelistLocked(uid, entry.second);
} else {
// Need more time
if (DEBUG) {
@@ -1862,6 +1899,22 @@ public class DeviceIdleController extends SystemService
}
}
+ @GuardedBy("this")
+ private void onAppRemovedFromTempWhitelistLocked(int appId, String reason) {
+ if (DEBUG) {
+ Slog.d(TAG, "Removing appId " + appId + " from temp whitelist");
+ }
+ updateTempWhitelistAppIdsLocked(appId, false);
+ mHandler.obtainMessage(MSG_REPORT_TEMP_APP_WHITELIST_CHANGED, appId, 0)
+ .sendToTarget();
+ reportTempWhitelistChangedLocked();
+ try {
+ mBatteryStats.noteEvent(BatteryStats.HistoryItem.EVENT_TEMP_WHITELIST_FINISH,
+ reason, appId);
+ } catch (RemoteException e) {
+ }
+ }
+
public void exitIdleInternal(String reason) {
synchronized (this) {
becomeActiveLocked(reason, Binder.getCallingUid());
@@ -2699,9 +2752,11 @@ public class DeviceIdleController extends SystemService
+ "changes made using this won't be persisted across boots");
pw.println(" tempwhitelist");
pw.println(" Print packages that are temporarily whitelisted.");
- pw.println(" tempwhitelist [-u USER] [-d DURATION] [package ..]");
- pw.println(" Temporarily place packages in whitelist for DURATION milliseconds.");
+ pw.println(" tempwhitelist [-u USER] [-d DURATION] [-r] [package]");
+ pw.println(" Temporarily place package in whitelist for DURATION milliseconds.");
pw.println(" If no DURATION is specified, 10 seconds is used");
+ pw.println(" If [-r] option is used, then the package is removed from temp whitelist "
+ + "and any [-d] is ignored");
}
class Shell extends ShellCommand {
@@ -2773,7 +2828,7 @@ public class DeviceIdleController extends SystemService
becomeInactiveIfAppropriateLocked();
int curLightState = mLightState;
while (curLightState != LIGHT_STATE_IDLE) {
- stepIdleStateLocked("s:shell");
+ stepLightIdleStateLocked("s:shell");
if (curLightState == mLightState) {
pw.print("Unable to go light idle; stopped at ");
pw.println(lightStateToString(mLightState));
@@ -2985,6 +3040,7 @@ public class DeviceIdleController extends SystemService
}
} else if ("tempwhitelist".equals(cmd)) {
long duration = 10000;
+ boolean removePkg = false;
String opt;
while ((opt=shell.getNextOption()) != null) {
if ("-u".equals(opt)) {
@@ -3001,16 +3057,25 @@ public class DeviceIdleController extends SystemService
return -1;
}
duration = Long.parseLong(opt);
+ } else if ("-r".equals(opt)) {
+ removePkg = true;
}
}
String arg = shell.getNextArg();
if (arg != null) {
try {
- addPowerSaveTempWhitelistAppChecked(arg, duration, shell.userId, "shell");
+ if (removePkg) {
+ removePowerSaveTempWhitelistAppChecked(arg, shell.userId);
+ } else {
+ addPowerSaveTempWhitelistAppChecked(arg, duration, shell.userId, "shell");
+ }
} catch (Exception e) {
pw.println("Failed: " + e);
return -1;
}
+ } else if (removePkg) {
+ pw.println("[-r] requires a package name");
+ return -1;
} else {
dumpTempWhitelistSchedule(pw, false);
}
diff --git a/com/android/server/DisplayThread.java b/com/android/server/DisplayThread.java
index cad2a612..85c799ca 100644
--- a/com/android/server/DisplayThread.java
+++ b/com/android/server/DisplayThread.java
@@ -40,7 +40,7 @@ public final class DisplayThread extends ServiceThread {
if (sInstance == null) {
sInstance = new DisplayThread();
sInstance.start();
- sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER);
sHandler = new Handler(sInstance.getLooper());
}
}
diff --git a/com/android/server/EntropyMixer.java b/com/android/server/EntropyMixer.java
index 24d8d1e8..98777179 100644
--- a/com/android/server/EntropyMixer.java
+++ b/com/android/server/EntropyMixer.java
@@ -70,7 +70,10 @@ public class EntropyMixer extends Binder {
/**
* Handler that periodically updates the entropy on disk.
*/
- private final Handler mHandler = new Handler() {
+ private final Handler mHandler = new Handler(IoThread.getHandler().getLooper()) {
+ // IMPLEMENTATION NOTE: This handler runs on the I/O thread to avoid I/O on the main thread.
+ // The reason we're using our own Handler instead of IoThread.getHandler() is to create our
+ // own ID space for the "what" parameter of messages seen by the handler.
@Override
public void handleMessage(Message msg) {
if (msg.what != ENTROPY_WHAT) {
@@ -115,7 +118,12 @@ public class EntropyMixer extends Binder {
IntentFilter broadcastFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
broadcastFilter.addAction(Intent.ACTION_POWER_CONNECTED);
broadcastFilter.addAction(Intent.ACTION_REBOOT);
- context.registerReceiver(mBroadcastReceiver, broadcastFilter);
+ context.registerReceiver(
+ mBroadcastReceiver,
+ broadcastFilter,
+ null, // do not require broadcaster to hold any permissions
+ mHandler // process received broadcasts on the I/O thread instead of the main thread
+ );
}
private void scheduleEntropyWriter() {
diff --git a/com/android/server/FgThread.java b/com/android/server/FgThread.java
index 18fb4778..021bfaa1 100644
--- a/com/android/server/FgThread.java
+++ b/com/android/server/FgThread.java
@@ -39,7 +39,7 @@ public final class FgThread extends ServiceThread {
if (sInstance == null) {
sInstance = new FgThread();
sInstance.start();
- sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER);
sHandler = new Handler(sInstance.getLooper());
}
}
diff --git a/com/android/server/GraphicsStatsService.java b/com/android/server/GraphicsStatsService.java
index d3f77b64..4639d758 100644
--- a/com/android/server/GraphicsStatsService.java
+++ b/com/android/server/GraphicsStatsService.java
@@ -175,7 +175,8 @@ public class GraphicsStatsService extends IGraphicsStats.Stub {
0,
UserHandle.getUserId(uid));
synchronized (mLock) {
- pfd = requestBufferForProcessLocked(token, uid, pid, packageName, info.versionCode);
+ pfd = requestBufferForProcessLocked(token, uid, pid, packageName,
+ info.getLongVersionCode());
}
} catch (PackageManager.NameNotFoundException ex) {
throw new RemoteException("Unable to find package: '" + packageName + "'");
@@ -197,7 +198,7 @@ public class GraphicsStatsService extends IGraphicsStats.Stub {
}
private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token,
- int uid, int pid, String packageName, int versionCode) throws RemoteException {
+ int uid, int pid, String packageName, long versionCode) throws RemoteException {
ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode);
scheduleRotateLocked();
return getPfd(buffer.mProcessBuffer);
@@ -292,7 +293,7 @@ public class GraphicsStatsService extends IGraphicsStats.Stub {
}
private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid,
- String packageName, int versionCode) throws RemoteException {
+ String packageName, long versionCode) throws RemoteException {
int size = mActive.size();
long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis();
for (int i = 0; i < size; i++) {
@@ -381,19 +382,19 @@ public class GraphicsStatsService extends IGraphicsStats.Stub {
private static native int nGetAshmemSize();
private static native long nCreateDump(int outFd, boolean isProto);
private static native void nAddToDump(long dump, String path, String packageName,
- int versionCode, long startTime, long endTime, byte[] data);
+ long versionCode, long startTime, long endTime, byte[] data);
private static native void nAddToDump(long dump, String path);
private static native void nFinishDump(long dump);
- private static native void nSaveBuffer(String path, String packageName, int versionCode,
+ private static native void nSaveBuffer(String path, String packageName, long versionCode,
long startTime, long endTime, byte[] data);
private final class BufferInfo {
final String packageName;
- final int versionCode;
+ final long versionCode;
long startTime;
long endTime;
- BufferInfo(String packageName, int versionCode, long startTime) {
+ BufferInfo(String packageName, long versionCode, long startTime) {
this.packageName = packageName;
this.versionCode = versionCode;
this.startTime = startTime;
@@ -408,7 +409,8 @@ public class GraphicsStatsService extends IGraphicsStats.Stub {
final IBinder mToken;
MemoryFile mProcessBuffer;
- ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode)
+ ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName,
+ long versionCode)
throws RemoteException, IOException {
mInfo = new BufferInfo(packageName, versionCode, System.currentTimeMillis());
mUid = uid;
diff --git a/com/android/server/InputMethodManagerService.java b/com/android/server/InputMethodManagerService.java
index fc57a0d5..55046959 100644
--- a/com/android/server/InputMethodManagerService.java
+++ b/com/android/server/InputMethodManagerService.java
@@ -35,6 +35,7 @@ import com.android.internal.os.SomeArgs;
import com.android.internal.os.TransferPipe;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.view.IInputContext;
import com.android.internal.view.IInputMethod;
import com.android.internal.view.IInputMethodClient;
@@ -49,12 +50,14 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
+import android.Manifest;
import android.annotation.BinderThread;
import android.annotation.ColorInt;
import android.annotation.IntDef;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
@@ -104,10 +107,14 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
+import android.os.ShellCallback;
+import android.os.ShellCommand;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
+import android.service.vr.IVrManager;
+import android.service.vr.IVrStateCallbacks;
import android.text.TextUtils;
import android.text.style.SuggestionSpan;
import android.util.ArrayMap;
@@ -176,6 +183,13 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
static final boolean DEBUG_RESTORE = DEBUG || false;
static final String TAG = "InputMethodManagerService";
+ @Retention(SOURCE)
+ @IntDef({ShellCommandResult.SUCCESS, ShellCommandResult.FAILURE})
+ private @interface ShellCommandResult {
+ int SUCCESS = 0;
+ int FAILURE = -1;
+ }
+
static final int MSG_SHOW_IM_SUBTYPE_PICKER = 1;
static final int MSG_SHOW_IM_SUBTYPE_ENABLER = 2;
static final int MSG_SHOW_IM_CONFIG = 3;
@@ -189,6 +203,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
static final int MSG_CREATE_SESSION = 1050;
static final int MSG_START_INPUT = 2000;
+ static final int MSG_START_VR_INPUT = 2010;
static final int MSG_UNBIND_CLIENT = 3000;
static final int MSG_BIND_CLIENT = 3010;
@@ -255,8 +270,6 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
private final AppOpsManager mAppOpsManager;
private final UserManager mUserManager;
- final InputBindResult mNoBinding = new InputBindResult(null, null, null, -1, -1);
-
// All known input methods. mMethodMap also serves as the global
// lock for this class.
final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>();
@@ -317,6 +330,28 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
}
+ /**
+ * VR state callback.
+ * Listens for when VR mode finishes.
+ */
+ private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
+ @Override
+ public void onVrStateChanged(boolean enabled) {
+ if (!enabled) {
+ restoreNonVrImeFromSettingsNoCheck();
+ }
+ }
+ };
+
+ private void restoreNonVrImeFromSettingsNoCheck() {
+ // switch back to non-VR InputMethod from settings.
+ synchronized (mMethodMap) {
+ final String lastInputId = mSettings.getSelectedInputMethod();
+ setInputMethodLocked(lastInputId,
+ mSettings.getSelectedInputMethodSubtypeId(lastInputId));
+ }
+ }
+
static final class ClientState {
final IInputMethodClient client;
final IInputContext inputContext;
@@ -863,6 +898,30 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
/**
+ * Start a VR InputMethod that matches IME with package name of {@param component}.
+ * Note: This method is called from {@link android.app.VrManager}.
+ */
+ private void startVrInputMethodNoCheck(@Nullable ComponentName component) {
+ if (component == null) {
+ // clear the current VR-only IME (if any) and restore normal IME.
+ restoreNonVrImeFromSettingsNoCheck();
+ return;
+ }
+
+ synchronized (mMethodMap) {
+ String packageName = component.getPackageName();
+ for (InputMethodInfo info : mMethodList) {
+ if (TextUtils.equals(info.getPackageName(), packageName) && info.isVrOnly()) {
+ // set this is as current inputMethod without updating settings.
+ setInputMethodEnabledLocked(info.getId(), true);
+ setInputMethodLocked(info.getId(), NOT_A_SUBTYPE_ID);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
* Handles {@link Intent#ACTION_LOCALE_CHANGED}.
*
* <p>Note: For historical reasons, {@link Intent#ACTION_LOCALE_CHANGED} has been sent to all
@@ -1338,6 +1397,15 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
mFileManager = new InputMethodFileManager(mMethodMap, userId);
mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
mSettings, context);
+ // Register VR-state listener.
+ IVrManager vrManager = (IVrManager) ServiceManager.getService(Context.VR_SERVICE);
+ if (vrManager != null) {
+ try {
+ vrManager.registerListener(mVrStateCallbacks);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to register VR mode state listener.");
+ }
+ }
}
private void resetDefaultImeLocked(Context context) {
@@ -1562,12 +1630,27 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
@Override
public List<InputMethodInfo> getInputMethodList() {
+ return getInputMethodList(false /* isVrOnly */);
+ }
+
+ public List<InputMethodInfo> getVrInputMethodList() {
+ return getInputMethodList(true /* isVrOnly */);
+ }
+
+ private List<InputMethodInfo> getInputMethodList(final boolean isVrOnly) {
// TODO: Make this work even for non-current users?
if (!calledFromValidUser()) {
return Collections.emptyList();
}
synchronized (mMethodMap) {
- return new ArrayList<>(mMethodList);
+ ArrayList<InputMethodInfo> methodList = new ArrayList<>();
+ for (InputMethodInfo info : mMethodList) {
+
+ if (info.isVrOnly() == isVrOnly) {
+ methodList.add(info);
+ }
+ }
+ return methodList;
}
}
@@ -1693,6 +1776,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
return flags;
}
+ @NonNull
InputBindResult attachNewInputLocked(
/* @InputMethodClient.StartInputReason */ final int startInputReason, boolean initial) {
if (!mBoundToMethod) {
@@ -1716,11 +1800,12 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
showCurrentInputLocked(getAppShowFlags(), null);
}
- return new InputBindResult(session.session,
- (session.channel != null ? session.channel.dup() : null),
+ return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
+ session.session, (session.channel != null ? session.channel.dup() : null),
mCurId, mCurSeq, mCurUserActionNotificationSequenceNumber);
}
+ @NonNull
InputBindResult startInputLocked(
/* @InputMethodClient.StartInputReason */ final int startInputReason,
IInputMethodClient client, IInputContext inputContext,
@@ -1728,7 +1813,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
@Nullable EditorInfo attribute, int controlFlags) {
// If no method is currently selected, do nothing.
if (mCurMethodId == null) {
- return mNoBinding;
+ return InputBindResult.NO_IME;
}
ClientState cs = mClients.get(client.asBinder());
@@ -1740,7 +1825,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
if (attribute == null) {
Slog.w(TAG, "Ignoring startInput with null EditorInfo."
+ " uid=" + cs.uid + " pid=" + cs.pid);
- return null;
+ return InputBindResult.NULL_EDITOR_INFO;
}
try {
@@ -1754,7 +1839,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
Slog.w(TAG, "Starting input on non-focused client " + cs.client
+ " (uid=" + cs.uid + " pid=" + cs.pid + ")");
}
- return null;
+ return InputBindResult.NOT_IME_TARGET_WINDOW;
}
} catch (RemoteException e) {
}
@@ -1763,20 +1848,21 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
controlFlags, startInputReason);
}
+ @NonNull
InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,
/* @InputConnectionInspector.missingMethods */ final int missingMethods,
@NonNull EditorInfo attribute, int controlFlags,
/* @InputMethodClient.StartInputReason */ final int startInputReason) {
// If no method is currently selected, do nothing.
if (mCurMethodId == null) {
- return mNoBinding;
+ return InputBindResult.NO_IME;
}
if (!InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, cs.uid,
attribute.packageName)) {
Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
+ " uid=" + cs.uid + " package=" + attribute.packageName);
- return mNoBinding;
+ return InputBindResult.INVALID_PACKAGE_NAME;
}
if (mCurClient != cs) {
@@ -1816,7 +1902,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
// Return to client, and we will get back with it when
// we have had a session made for it.
requestClientSessionLocked(cs);
- return new InputBindResult(null, null, mCurId, mCurSeq,
+ return new InputBindResult(
+ InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
+ null, null, mCurId, mCurSeq,
mCurUserActionNotificationSequenceNumber);
} else if (SystemClock.uptimeMillis()
< (mLastBindTime+TIME_TO_RECONNECT)) {
@@ -1827,7 +1915,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
// we can report back. If it has been too long, we want
// to fall through so we can try a disconnect/reconnect
// to see if we can get back in touch with the service.
- return new InputBindResult(null, null, mCurId, mCurSeq,
+ return new InputBindResult(
+ InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
+ null, null, mCurId, mCurSeq,
mCurUserActionNotificationSequenceNumber);
} else {
EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME,
@@ -1841,13 +1931,15 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
InputBindResult startInputInnerLocked() {
if (mCurMethodId == null) {
- return mNoBinding;
+ return InputBindResult.NO_IME;
}
if (!mSystemReady) {
// If the system is not yet ready, we shouldn't be running third
// party code.
- return new InputBindResult(null, null, mCurMethodId, mCurSeq,
+ return new InputBindResult(
+ InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY,
+ null, null, mCurMethodId, mCurSeq,
mCurUserActionNotificationSequenceNumber);
}
@@ -1874,23 +1966,24 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
mIWindowManager.addWindowToken(mCurToken, TYPE_INPUT_METHOD, DEFAULT_DISPLAY);
} catch (RemoteException e) {
}
- return new InputBindResult(null, null, mCurId, mCurSeq,
+ return new InputBindResult(
+ InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
+ null, null, mCurId, mCurSeq,
mCurUserActionNotificationSequenceNumber);
- } else {
- mCurIntent = null;
- Slog.w(TAG, "Failure connecting to input method service: "
- + mCurIntent);
}
- return null;
+ mCurIntent = null;
+ Slog.w(TAG, "Failure connecting to input method service: " + mCurIntent);
+ return InputBindResult.IME_NOT_CONNECTED;
}
+ @NonNull
private InputBindResult startInput(
/* @InputMethodClient.StartInputReason */ final int startInputReason,
IInputMethodClient client, IInputContext inputContext,
/* @InputConnectionInspector.missingMethods */ final int missingMethods,
@Nullable EditorInfo attribute, int controlFlags) {
if (!calledFromValidUser()) {
- return null;
+ return InputBindResult.INVALID_USER;
}
synchronized (mMethodMap) {
if (DEBUG) {
@@ -2634,27 +2727,42 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
return res;
}
+ @NonNull
@Override
public InputBindResult startInputOrWindowGainedFocus(
/* @InputMethodClient.StartInputReason */ final int startInputReason,
IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,
int windowFlags, @Nullable EditorInfo attribute, IInputContext inputContext,
- /* @InputConnectionInspector.missingMethods */ final int missingMethods) {
+ /* @InputConnectionInspector.missingMethods */ final int missingMethods,
+ int unverifiedTargetSdkVersion) {
+ final InputBindResult result;
if (windowToken != null) {
- return windowGainedFocus(startInputReason, client, windowToken, controlFlags,
- softInputMode, windowFlags, attribute, inputContext, missingMethods);
+ result = windowGainedFocus(startInputReason, client, windowToken, controlFlags,
+ softInputMode, windowFlags, attribute, inputContext, missingMethods,
+ unverifiedTargetSdkVersion);
} else {
- return startInput(startInputReason, client, inputContext, missingMethods, attribute,
+ result = startInput(startInputReason, client, inputContext, missingMethods, attribute,
controlFlags);
}
+ if (result == null) {
+ // This must never happen, but just in case.
+ Slog.wtf(TAG, "InputBindResult is @NonNull. startInputReason="
+ + InputMethodClient.getStartInputReason(startInputReason)
+ + " windowFlags=#" + Integer.toHexString(windowFlags)
+ + " editorInfo=" + attribute);
+ return InputBindResult.NULL;
+ }
+ return result;
}
+ @NonNull
private InputBindResult windowGainedFocus(
/* @InputMethodClient.StartInputReason */ final int startInputReason,
IInputMethodClient client, IBinder windowToken, int controlFlags,
/* @android.view.WindowManager.LayoutParams.SoftInputModeFlags */ int softInputMode,
int windowFlags, EditorInfo attribute, IInputContext inputContext,
- /* @InputConnectionInspector.missingMethods */ final int missingMethods) {
+ /* @InputConnectionInspector.missingMethods */ final int missingMethods,
+ int unverifiedTargetSdkVersion) {
// Needs to check the validity before clearing calling identity
final boolean calledFromValidUser = calledFromValidUser();
InputBindResult res = null;
@@ -2670,7 +2778,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
+ " attribute=" + attribute
+ " controlFlags=#" + Integer.toHexString(controlFlags)
+ " softInputMode=" + InputMethodClient.softInputModeToString(softInputMode)
- + " windowFlags=#" + Integer.toHexString(windowFlags));
+ + " windowFlags=#" + Integer.toHexString(windowFlags)
+ + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion);
ClientState cs = mClients.get(client.asBinder());
if (cs == null) {
@@ -2689,7 +2798,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
Slog.w(TAG, "Focus gain on non-focused client " + cs.client
+ " (uid=" + cs.uid + " pid=" + cs.pid + ")");
}
- return null;
+ return InputBindResult.NOT_IME_TARGET_WINDOW;
}
} catch (RemoteException e) {
}
@@ -2699,7 +2808,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
Slog.w(TAG, "If you want to interect with IME, you need "
+ "android.permission.INTERACT_ACROSS_USERS_FULL");
hideCurrentInputLocked(0, null);
- return null;
+ return InputBindResult.INVALID_USER;
}
if (mCurFocusedWindow == windowToken) {
@@ -2711,7 +2820,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
return startInputUncheckedLocked(cs, inputContext, missingMethods,
attribute, controlFlags, startInputReason);
}
- return null;
+ return new InputBindResult(
+ InputBindResult.ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
+ null, null, null, -1, -1);
}
mCurFocusedWindow = windowToken;
mCurFocusedWindowSoftInputMode = softInputMode;
@@ -2784,22 +2895,37 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
if ((softInputMode &
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
- if (attribute != null) {
- res = startInputUncheckedLocked(cs, inputContext,
- missingMethods, attribute, controlFlags, startInputReason);
- didStart = true;
+ if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(
+ unverifiedTargetSdkVersion, controlFlags)) {
+ if (attribute != null) {
+ res = startInputUncheckedLocked(cs, inputContext,
+ missingMethods, attribute, controlFlags,
+ startInputReason);
+ didStart = true;
+ }
+ showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
+ } else {
+ Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
+ + " there is no focused view that also returns true from"
+ + " View#onCheckIsTextEditor()");
}
- showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
}
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
if (DEBUG) Slog.v(TAG, "Window asks to always show input");
- if (attribute != null) {
- res = startInputUncheckedLocked(cs, inputContext, missingMethods,
- attribute, controlFlags, startInputReason);
- didStart = true;
+ if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(
+ unverifiedTargetSdkVersion, controlFlags)) {
+ if (attribute != null) {
+ res = startInputUncheckedLocked(cs, inputContext, missingMethods,
+ attribute, controlFlags, startInputReason);
+ didStart = true;
+ }
+ showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
+ } else {
+ Slog.e(TAG, "SOFT_INPUT_STATE_ALWAYS_VISIBLE is ignored because"
+ + " there is no focused view that also returns true from"
+ + " View#onCheckIsTextEditor()");
}
- showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
break;
}
@@ -3356,6 +3482,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
case MSG_SET_INTERACTIVE:
handleSetInteractive(msg.arg1 != 0);
return true;
+ case MSG_START_VR_INPUT:
+ startVrInputMethodNoCheck((ComponentName) msg.obj);
+ return true;
case MSG_SWITCH_IME:
handleSwitchInputMethod(msg.arg1 != 0);
return true;
@@ -3809,30 +3938,6 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
// ----------------------------------------------------------------------
- @Override
- public boolean setInputMethodEnabled(String id, boolean enabled) {
- // TODO: Make this work even for non-current users?
- if (!calledFromValidUser()) {
- return false;
- }
- synchronized (mMethodMap) {
- if (mContext.checkCallingOrSelfPermission(
- android.Manifest.permission.WRITE_SECURE_SETTINGS)
- != PackageManager.PERMISSION_GRANTED) {
- throw new SecurityException(
- "Requires permission "
- + android.Manifest.permission.WRITE_SECURE_SETTINGS);
- }
-
- long ident = Binder.clearCallingIdentity();
- try {
- return setInputMethodEnabledLocked(id, enabled);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
- }
-
boolean setInputMethodEnabledLocked(String id, boolean enabled) {
// Make sure this is a valid input method.
InputMethodInfo imm = mMethodMap.get(id);
@@ -3876,8 +3981,12 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeId,
boolean setSubtypeOnly) {
- // Update the history of InputMethod and Subtype
- mSettings.saveCurrentInputMethodAndSubtypeToHistory(mCurMethodId, mCurrentSubtype);
+ // Updates to InputMethod are transient in VR mode. Its not included in history.
+ final boolean isVrInput = imi != null && imi.isVrOnly();
+ if (!isVrInput) {
+ // Update the history of InputMethod and Subtype
+ mSettings.saveCurrentInputMethodAndSubtypeToHistory(mCurMethodId, mCurrentSubtype);
+ }
mCurUserActionNotificationSequenceNumber =
Math.max(mCurUserActionNotificationSequenceNumber + 1, 1);
@@ -3892,6 +4001,11 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
mCurUserActionNotificationSequenceNumber, mCurClient));
}
+ if (isVrInput) {
+ // Updates to InputMethod are transient in VR mode. Any changes to Settings are skipped.
+ return;
+ }
+
// Set Subtype here
if (imi == null || subtypeId < 0) {
mSettings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
@@ -4351,6 +4465,11 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
mHandler.removeMessages(MSG_HIDE_CURRENT_INPUT_METHOD);
mHandler.sendEmptyMessage(MSG_HIDE_CURRENT_INPUT_METHOD);
}
+
+ @Override
+ public void startVrInputMethodNoCheck(@Nullable ComponentName componentName) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_START_VR_INPUT, componentName));
+ }
}
private static String imeWindowStatusToString(final int imeWindowVis) {
@@ -4543,4 +4662,294 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
p.println("No input method service.");
}
}
+
+ @BinderThread
+ @Override
+ public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+ @Nullable FileDescriptor err,
+ @NonNull String[] args, @Nullable ShellCallback callback,
+ @NonNull ResultReceiver resultReceiver) throws RemoteException {
+ new ShellCommandImpl(this).exec(
+ this, in, out, err, args, callback, resultReceiver);
+ }
+
+ private static final class ShellCommandImpl extends ShellCommand {
+ @NonNull
+ final InputMethodManagerService mService;
+
+ ShellCommandImpl(InputMethodManagerService service) {
+ mService = service;
+ }
+
+ @BinderThread
+ @ShellCommandResult
+ @Override
+ public int onCommand(@Nullable String cmd) {
+ // For existing "adb shell ime <command>".
+ if ("ime".equals(cmd)) {
+ final String imeCommand = getNextArg();
+ if (imeCommand == null || "help".equals(imeCommand) || "-h".equals(imeCommand)) {
+ onImeCommandHelp();
+ return ShellCommandResult.SUCCESS;
+ }
+ switch (imeCommand) {
+ case "list":
+ return mService.handleShellCommandListInputMethods(this);
+ case "enable":
+ return mService.handleShellCommandEnableDisableInputMethod(this, true);
+ case "disable":
+ return mService.handleShellCommandEnableDisableInputMethod(this, false);
+ case "set":
+ return mService.handleShellCommandSetInputMethod(this);
+ case "reset":
+ return mService.handleShellCommandResetInputMethod(this);
+ default:
+ getOutPrintWriter().println("Unknown command: " + imeCommand);
+ return ShellCommandResult.FAILURE;
+ }
+ }
+
+ return handleDefaultCommands(cmd);
+ }
+
+ @BinderThread
+ @Override
+ public void onHelp() {
+ try (PrintWriter pw = getOutPrintWriter()) {
+ pw.println("InputMethodManagerService commands:");
+ pw.println(" help");
+ pw.println(" Prints this help text.");
+ pw.println(" dump [options]");
+ pw.println(" Synonym of dumpsys.");
+ pw.println(" ime <command> [options]");
+ pw.println(" Manipulate IMEs. Run \"ime help\" for details.");
+ }
+ }
+
+ private void onImeCommandHelp() {
+ try (IndentingPrintWriter pw =
+ new IndentingPrintWriter(getOutPrintWriter(), " ", 100)) {
+ pw.println("ime <command>:");
+ pw.increaseIndent();
+
+ pw.println("list [-a] [-s]");
+ pw.increaseIndent();
+ pw.println("prints all enabled input methods.");
+ pw.increaseIndent();
+ pw.println("-a: see all input methods");
+ pw.println("-s: only a single summary line of each");
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+
+ pw.println("enable <ID>");
+ pw.increaseIndent();
+ pw.println("allows the given input method ID to be used.");
+ pw.decreaseIndent();
+
+ pw.println("disable <ID>");
+ pw.increaseIndent();
+ pw.println("disallows the given input method ID to be used.");
+ pw.decreaseIndent();
+
+ pw.println("set <ID>");
+ pw.increaseIndent();
+ pw.println("switches to the given input method ID.");
+ pw.decreaseIndent();
+
+ pw.println("reset");
+ pw.increaseIndent();
+ pw.println("reset currently selected/enabled IMEs to the default ones as if "
+ + "the device is initially booted with the current locale.");
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+ }
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // Shell command handlers:
+
+ /**
+ * Handles {@code adb shell ime list}.
+ * @param shellCommand {@link ShellCommand} object that is handling this command.
+ * @return Exit code of the command.
+ */
+ @BinderThread
+ @ShellCommandResult
+ private int handleShellCommandListInputMethods(@NonNull ShellCommand shellCommand) {
+ boolean all = false;
+ boolean brief = false;
+ while (true) {
+ final String nextOption = shellCommand.getNextOption();
+ if (nextOption == null) {
+ break;
+ }
+ switch (nextOption) {
+ case "-a":
+ all = true;
+ break;
+ case "-s":
+ brief = true;
+ break;
+ }
+ }
+ final List<InputMethodInfo> methods = all ?
+ getInputMethodList() : getEnabledInputMethodList();
+ final PrintWriter pr = shellCommand.getOutPrintWriter();
+ final Printer printer = x -> pr.println(x);
+ final int N = methods.size();
+ for (int i = 0; i < N; ++i) {
+ if (brief) {
+ pr.println(methods.get(i).getId());
+ } else {
+ pr.print(methods.get(i).getId()); pr.println(":");
+ methods.get(i).dump(printer, " ");
+ }
+ }
+ return ShellCommandResult.SUCCESS;
+ }
+
+ /**
+ * Handles {@code adb shell ime enable} and {@code adb shell ime disable}.
+ * @param shellCommand {@link ShellCommand} object that is handling this command.
+ * @param enabled {@code true} if the command was {@code adb shell ime enable}.
+ * @return Exit code of the command.
+ */
+ @BinderThread
+ @ShellCommandResult
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ private int handleShellCommandEnableDisableInputMethod(
+ @NonNull ShellCommand shellCommand, boolean enabled) {
+ if (!calledFromValidUser()) {
+ shellCommand.getErrPrintWriter().print(
+ "Must be called from the foreground user or with INTERACT_ACROSS_USERS_FULL");
+ return ShellCommandResult.FAILURE;
+ }
+ final String id = shellCommand.getNextArgRequired();
+
+ final boolean previouslyEnabled;
+ synchronized (mMethodMap) {
+ if (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ shellCommand.getErrPrintWriter().print(
+ "Caller must have WRITE_SECURE_SETTINGS permission");
+ throw new SecurityException(
+ "Requires permission "
+ + android.Manifest.permission.WRITE_SECURE_SETTINGS);
+ }
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ previouslyEnabled = setInputMethodEnabledLocked(id, enabled);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ final PrintWriter pr = shellCommand.getOutPrintWriter();
+ pr.print("Input method ");
+ pr.print(id);
+ pr.print(": ");
+ pr.print((enabled == previouslyEnabled) ? "already " : "now ");
+ pr.println(enabled ? "enabled" : "disabled");
+ return ShellCommandResult.SUCCESS;
+ }
+
+ /**
+ * Handles {@code adb shell ime set}.
+ * @param shellCommand {@link ShellCommand} object that is handling this command.
+ * @return Exit code of the command.
+ */
+ @BinderThread
+ @ShellCommandResult
+ private int handleShellCommandSetInputMethod(@NonNull ShellCommand shellCommand) {
+ final String id = shellCommand.getNextArgRequired();
+ setInputMethod(null, id);
+ final PrintWriter pr = shellCommand.getOutPrintWriter();
+ pr.print("Input method ");
+ pr.print(id);
+ pr.println(" selected");
+ return ShellCommandResult.SUCCESS;
+ }
+
+ /**
+ * Handles {@code adb shell ime reset-ime}.
+ * @param shellCommand {@link ShellCommand} object that is handling this command.
+ * @return Exit code of the command.
+ */
+ @BinderThread
+ @ShellCommandResult
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ private int handleShellCommandResetInputMethod(@NonNull ShellCommand shellCommand) {
+ if (!calledFromValidUser()) {
+ shellCommand.getErrPrintWriter().print(
+ "Must be called from the foreground user or with INTERACT_ACROSS_USERS_FULL");
+ return ShellCommandResult.FAILURE;
+ }
+ synchronized (mMethodMap) {
+ if (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ shellCommand.getErrPrintWriter().print(
+ "Caller must have WRITE_SECURE_SETTINGS permission");
+ throw new SecurityException(
+ "Requires permission "
+ + android.Manifest.permission.WRITE_SECURE_SETTINGS);
+ }
+ final String nextIme;
+ final List<InputMethodInfo> nextEnabledImes;
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mMethodMap) {
+ hideCurrentInputLocked(0, null);
+ unbindCurrentMethodLocked(false);
+ // Reset the current IME
+ resetSelectedInputMethodAndSubtypeLocked(null);
+ // Also reset the settings of the current IME
+ mSettings.putSelectedInputMethod(null);
+ // Disable all enabled IMEs.
+ {
+ final ArrayList<InputMethodInfo> enabledImes =
+ mSettings.getEnabledInputMethodListLocked();
+ final int N = enabledImes.size();
+ for (int i = 0; i < N; ++i) {
+ setInputMethodEnabledLocked(enabledImes.get(i).getId(), false);
+ }
+ }
+ // Re-enable with default enabled IMEs.
+ {
+ final ArrayList<InputMethodInfo> defaultEnabledIme =
+ InputMethodUtils.getDefaultEnabledImes(mContext, mMethodList);
+ final int N = defaultEnabledIme.size();
+ for (int i = 0; i < N; ++i) {
+ setInputMethodEnabledLocked(defaultEnabledIme.get(i).getId(), true);
+ }
+ }
+ updateInputMethodsFromSettingsLocked(true /* enabledMayChange */);
+ InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(mIPackageManager,
+ mSettings.getEnabledInputMethodListLocked(),
+ mSettings.getCurrentUserId(),
+ mContext.getBasePackageName());
+ nextIme = mSettings.getSelectedInputMethod();
+ nextEnabledImes = getEnabledInputMethodList();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ final PrintWriter pr = shellCommand.getOutPrintWriter();
+ pr.println("Reset current and enabled IMEs");
+ pr.println("Newly selected IME:");
+ pr.print(" "); pr.println(nextIme);
+ pr.println("Newly enabled IMEs:");
+ {
+ final int N = nextEnabledImes.size();
+ for (int i = 0; i < N; ++i) {
+ pr.print(" ");
+ pr.println(nextEnabledImes.get(i).getId());
+ }
+ }
+ return ShellCommandResult.SUCCESS;
+ }
+ }
}
diff --git a/com/android/server/IoThread.java b/com/android/server/IoThread.java
index ad4c1948..bfe825a3 100644
--- a/com/android/server/IoThread.java
+++ b/com/android/server/IoThread.java
@@ -36,7 +36,7 @@ public final class IoThread extends ServiceThread {
if (sInstance == null) {
sInstance = new IoThread();
sInstance.start();
- sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER);
sHandler = new Handler(sInstance.getLooper());
}
}
diff --git a/com/android/server/IpSecService.java b/com/android/server/IpSecService.java
index 1154fbe6..d3ab1259 100644
--- a/com/android/server/IpSecService.java
+++ b/com/android/server/IpSecService.java
@@ -34,6 +34,7 @@ import android.net.IpSecTransform;
import android.net.IpSecTransformResponse;
import android.net.IpSecUdpEncapResponse;
import android.net.NetworkUtils;
+import android.net.TrafficStats;
import android.net.util.NetdService;
import android.os.Binder;
import android.os.IBinder;
@@ -57,11 +58,23 @@ import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import libcore.io.IoUtils;
-/** @hide */
+/**
+ * A service to manage multiple clients that want to access the IpSec API. The service is
+ * responsible for maintaining a list of clients and managing the resources (and related quotas)
+ * that each of them own.
+ *
+ * <p>Synchronization in IpSecService is done on all entrypoints due to potential race conditions at
+ * the kernel/xfrm level. Further, this allows the simplifying assumption to be made that only one
+ * thread is ever running at a time.
+ *
+ * @hide
+ */
public class IpSecService extends IIpSecService.Stub {
private static final String TAG = "IpSecService";
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
@@ -91,17 +104,6 @@ public class IpSecService extends IIpSecService.Stub {
/** Should be a never-repeating global ID for resources */
private static AtomicInteger mNextResourceId = new AtomicInteger(0x00FADED0);
- @GuardedBy("this")
- private final ManagedResourceArray<SpiRecord> mSpiRecords = new ManagedResourceArray<>();
-
- @GuardedBy("this")
- private final ManagedResourceArray<TransformRecord> mTransformRecords =
- new ManagedResourceArray<>();
-
- @GuardedBy("this")
- private final ManagedResourceArray<UdpSocketRecord> mUdpSocketRecords =
- new ManagedResourceArray<>();
-
interface IpSecServiceConfiguration {
INetd getNetdInstance() throws RemoteException;
@@ -119,9 +121,178 @@ public class IpSecService extends IIpSecService.Stub {
}
private final IpSecServiceConfiguration mSrvConfig;
+ final UidFdTagger mUidFdTagger;
+
+ /**
+ * Interface for user-reference and kernel-resource cleanup.
+ *
+ * <p>This interface must be implemented for a resource to be reference counted.
+ */
+ @VisibleForTesting
+ public interface IResource {
+ /**
+ * Invalidates a IResource object, ensuring it is invalid for the purposes of allocating new
+ * objects dependent on it.
+ *
+ * <p>Implementations of this method are expected to remove references to the IResource
+ * object from the IpSecService's tracking arrays. The removal from the arrays ensures that
+ * the resource is considered invalid for user access or allocation or use in other
+ * resources.
+ *
+ * <p>References to the IResource object may be held by other RefcountedResource objects,
+ * and as such, the kernel resources and quota may not be cleaned up.
+ */
+ void invalidate() throws RemoteException;
+
+ /**
+ * Releases underlying resources and related quotas.
+ *
+ * <p>Implementations of this method are expected to remove all system resources that are
+ * tracked by the IResource object. Due to other RefcountedResource objects potentially
+ * having references to the IResource object, freeUnderlyingResources may not always be
+ * called from releaseIfUnreferencedRecursively().
+ */
+ void freeUnderlyingResources() throws RemoteException;
+ }
+
+ /**
+ * RefcountedResource manages references and dependencies in an exclusively acyclic graph.
+ *
+ * <p>RefcountedResource implements both explicit and implicit resource management. Creating a
+ * RefcountedResource object creates an explicit reference that must be freed by calling
+ * userRelease(). Additionally, adding this object as a child of another RefcountedResource
+ * object will add an implicit reference.
+ *
+ * <p>Resources are cleaned up when all references, both implicit and explicit, are released
+ * (ie, when userRelease() is called and when all parents have called releaseReference() on this
+ * object.)
+ */
+ @VisibleForTesting
+ public class RefcountedResource<T extends IResource> implements IBinder.DeathRecipient {
+ private final T mResource;
+ private final List<RefcountedResource> mChildren;
+ int mRefCount = 1; // starts at 1 for user's reference.
+ IBinder mBinder;
+
+ RefcountedResource(T resource, IBinder binder, RefcountedResource... children) {
+ synchronized (IpSecService.this) {
+ this.mResource = resource;
+ this.mChildren = new ArrayList<>(children.length);
+ this.mBinder = binder;
+
+ for (RefcountedResource child : children) {
+ mChildren.add(child);
+ child.mRefCount++;
+ }
+
+ try {
+ mBinder.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ binderDied();
+ }
+ }
+ }
+
+ /**
+ * If the Binder object dies, this function is called to free the system resources that are
+ * being tracked by this record and to subsequently release this record for garbage
+ * collection
+ */
+ @Override
+ public void binderDied() {
+ synchronized (IpSecService.this) {
+ try {
+ userRelease();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to release resource: " + e);
+ }
+ }
+ }
+
+ public T getResource() {
+ return mResource;
+ }
+
+ /**
+ * Unlinks from binder and performs IpSecService resource cleanup (removes from resource
+ * arrays)
+ *
+ * <p>If this method has been previously called, the RefcountedResource's binder field will
+ * be null, and the method will return without performing the cleanup a second time.
+ *
+ * <p>Note that calling this function does not imply that kernel resources will be freed at
+ * this time, or that the related quota will be returned. Such actions will only be
+ * performed upon the reference count reaching zero.
+ */
+ @GuardedBy("IpSecService.this")
+ public void userRelease() throws RemoteException {
+ // Prevent users from putting reference counts into a bad state by calling
+ // userRelease() multiple times.
+ if (mBinder == null) {
+ return;
+ }
+
+ mBinder.unlinkToDeath(this, 0);
+ mBinder = null;
+
+ mResource.invalidate();
+
+ releaseReference();
+ }
+
+ /**
+ * Removes a reference to this resource. If the resultant reference count is zero, the
+ * underlying resources are freed, and references to all child resources are also dropped
+ * recursively (resulting in them freeing their resources and children, etcetera)
+ *
+ * <p>This method also sets the reference count to an invalid value (-1) to signify that it
+ * has been fully released. Any subsequent calls to this method will result in an
+ * IllegalStateException being thrown due to resource already having been previously
+ * released
+ */
+ @VisibleForTesting
+ @GuardedBy("IpSecService.this")
+ public void releaseReference() throws RemoteException {
+ mRefCount--;
+
+ if (mRefCount > 0) {
+ return;
+ } else if (mRefCount < 0) {
+ throw new IllegalStateException(
+ "Invalid operation - resource has already been released.");
+ }
+
+ // Cleanup own resources
+ mResource.freeUnderlyingResources();
+
+ // Cleanup child resources as needed
+ for (RefcountedResource<? extends IResource> child : mChildren) {
+ child.releaseReference();
+ }
+
+ // Enforce that resource cleanup can only be called once
+ // By decrementing the refcount (from 0 to -1), the next call will throw an
+ // IllegalStateException - it has already been released fully.
+ mRefCount--;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("{mResource=")
+ .append(mResource)
+ .append(", mRefCount=")
+ .append(mRefCount)
+ .append(", mChildren=")
+ .append(mChildren)
+ .append("}")
+ .toString();
+ }
+ }
/* Very simple counting class that looks much like a counting semaphore */
- public static class ResourceTracker {
+ @VisibleForTesting
+ static class ResourceTracker {
private final int mMax;
int mCurrent;
@@ -130,18 +301,18 @@ public class IpSecService extends IIpSecService.Stub {
mCurrent = 0;
}
- synchronized boolean isAvailable() {
+ boolean isAvailable() {
return (mCurrent < mMax);
}
- synchronized void take() {
+ void take() {
if (!isAvailable()) {
Log.wtf(TAG, "Too many resources allocated!");
}
mCurrent++;
}
- synchronized void give() {
+ void give() {
if (mCurrent <= 0) {
Log.wtf(TAG, "We've released this resource too many times");
}
@@ -160,40 +331,70 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- private static final class UserQuotaTracker {
- /* Maximum number of UDP Encap Sockets that a single UID may possess */
- public static final int MAX_NUM_ENCAP_SOCKETS = 2;
+ @VisibleForTesting
+ static final class UserRecord {
+ /* Type names */
+ public static final String TYPENAME_SPI = "SecurityParameterIndex";
+ public static final String TYPENAME_TRANSFORM = "IpSecTransform";
+ public static final String TYPENAME_ENCAP_SOCKET = "UdpEncapSocket";
- /* Maximum number of IPsec Transforms that a single UID may possess */
+ /* Maximum number of each type of resource that a single UID may possess */
+ public static final int MAX_NUM_ENCAP_SOCKETS = 2;
public static final int MAX_NUM_TRANSFORMS = 4;
-
- /* Maximum number of IPsec Transforms that a single UID may possess */
public static final int MAX_NUM_SPIS = 8;
- /* Record for one users's IpSecService-managed objects */
- public static class UserRecord {
- public final ResourceTracker socket = new ResourceTracker(MAX_NUM_ENCAP_SOCKETS);
- public final ResourceTracker transform = new ResourceTracker(MAX_NUM_TRANSFORMS);
- public final ResourceTracker spi = new ResourceTracker(MAX_NUM_SPIS);
+ final RefcountedResourceArray<SpiRecord> mSpiRecords =
+ new RefcountedResourceArray<>(TYPENAME_SPI);
+ final ResourceTracker mSpiQuotaTracker = new ResourceTracker(MAX_NUM_SPIS);
- @Override
- public String toString() {
- return new StringBuilder()
- .append("{socket=")
- .append(socket)
- .append(", transform=")
- .append(transform)
- .append(", spi=")
- .append(spi)
- .append("}")
- .toString();
- }
+ final RefcountedResourceArray<TransformRecord> mTransformRecords =
+ new RefcountedResourceArray<>(TYPENAME_TRANSFORM);
+ final ResourceTracker mTransformQuotaTracker = new ResourceTracker(MAX_NUM_TRANSFORMS);
+
+ final RefcountedResourceArray<EncapSocketRecord> mEncapSocketRecords =
+ new RefcountedResourceArray<>(TYPENAME_ENCAP_SOCKET);
+ final ResourceTracker mSocketQuotaTracker = new ResourceTracker(MAX_NUM_ENCAP_SOCKETS);
+
+ void removeSpiRecord(int resourceId) {
+ mSpiRecords.remove(resourceId);
+ }
+
+ void removeTransformRecord(int resourceId) {
+ mTransformRecords.remove(resourceId);
+ }
+
+ void removeEncapSocketRecord(int resourceId) {
+ mEncapSocketRecords.remove(resourceId);
}
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("{mSpiQuotaTracker=")
+ .append(mSpiQuotaTracker)
+ .append(", mTransformQuotaTracker=")
+ .append(mTransformQuotaTracker)
+ .append(", mSocketQuotaTracker=")
+ .append(mSocketQuotaTracker)
+ .append(", mSpiRecords=")
+ .append(mSpiRecords)
+ .append(", mTransformRecords=")
+ .append(mTransformRecords)
+ .append(", mEncapSocketRecords=")
+ .append(mEncapSocketRecords)
+ .append("}")
+ .toString();
+ }
+ }
+
+ @VisibleForTesting
+ static final class UserResourceTracker {
private final SparseArray<UserRecord> mUserRecords = new SparseArray<>();
- /* a never-fail getter so that we can populate the list of UIDs as-needed */
- public synchronized UserRecord getUserRecord(int uid) {
+ /** Never-fail getter that populates the list of UIDs as-needed */
+ public UserRecord getUserRecord(int uid) {
+ checkCallerUid(uid);
+
UserRecord r = mUserRecords.get(uid);
if (r == null) {
r = new UserRecord();
@@ -202,122 +403,56 @@ public class IpSecService extends IIpSecService.Stub {
return r;
}
+ /** Safety method; guards against access of other user's UserRecords */
+ private void checkCallerUid(int uid) {
+ if (uid != Binder.getCallingUid()
+ && android.os.Process.SYSTEM_UID != Binder.getCallingUid()) {
+ throw new SecurityException("Attempted access of unowned resources");
+ }
+ }
+
@Override
public String toString() {
return mUserRecords.toString();
}
}
- private final UserQuotaTracker mUserQuotaTracker = new UserQuotaTracker();
+ @VisibleForTesting final UserResourceTracker mUserResourceTracker = new UserResourceTracker();
/**
- * The ManagedResource class provides a facility to cleanly and reliably release system
- * resources. It relies on two things: an IBinder that allows ManagedResource to automatically
- * clean up in the event that the Binder dies and a user-provided resourceId that should
- * uniquely identify the managed resource. To use this class, the user should implement the
- * releaseResources() method that is responsible for releasing system resources when invoked.
+ * The KernelResourceRecord class provides a facility to cleanly and reliably track system
+ * resources. It relies on a provided resourceId that should uniquely identify the kernel
+ * resource. To use this class, the user should implement the invalidate() and
+ * freeUnderlyingResources() methods that are responsible for cleaning up IpSecService resource
+ * tracking arrays and kernel resources, respectively
*/
- private abstract class ManagedResource implements IBinder.DeathRecipient {
+ private abstract class KernelResourceRecord implements IResource {
final int pid;
final int uid;
- private IBinder mBinder;
- protected int mResourceId;
-
- private AtomicInteger mReferenceCount = new AtomicInteger(0);
+ protected final int mResourceId;
- ManagedResource(int resourceId, IBinder binder) {
+ KernelResourceRecord(int resourceId) {
super();
if (resourceId == INVALID_RESOURCE_ID) {
throw new IllegalArgumentException("Resource ID must not be INVALID_RESOURCE_ID");
}
- mBinder = binder;
mResourceId = resourceId;
pid = Binder.getCallingPid();
uid = Binder.getCallingUid();
getResourceTracker().take();
- try {
- mBinder.linkToDeath(this, 0);
- } catch (RemoteException e) {
- binderDied();
- }
}
- public void addReference() {
- mReferenceCount.incrementAndGet();
- }
-
- public void removeReference() {
- if (mReferenceCount.decrementAndGet() < 0) {
- Log.wtf(TAG, "Programming error: negative reference count");
- }
- }
-
- public boolean isReferenced() {
- return (mReferenceCount.get() > 0);
- }
-
- /**
- * Ensures that the caller is either the owner of this resource or has the system UID and
- * throws a SecurityException otherwise.
- */
- public void checkOwnerOrSystem() {
- if (uid != Binder.getCallingUid()
- && android.os.Process.SYSTEM_UID != Binder.getCallingUid()) {
- throw new SecurityException("Only the owner may access managed resources!");
- }
- }
-
- /**
- * When this record is no longer needed for managing system resources this function should
- * clean up all system resources and nullify the record. This function shall perform all
- * necessary cleanup of the resources managed by this record.
- *
- * <p>NOTE: this function verifies ownership before allowing resources to be freed.
- */
- public final void release() throws RemoteException {
- synchronized (IpSecService.this) {
- if (isReferenced()) {
- throw new IllegalStateException(
- "Cannot release a resource that has active references!");
- }
-
- if (mResourceId == INVALID_RESOURCE_ID) {
- return;
- }
-
- releaseResources();
- getResourceTracker().give();
- if (mBinder != null) {
- mBinder.unlinkToDeath(this, 0);
- }
- mBinder = null;
+ @Override
+ public abstract void invalidate() throws RemoteException;
- mResourceId = INVALID_RESOURCE_ID;
- }
+ /** Convenience method; retrieves the user resource record for the stored UID. */
+ protected UserRecord getUserRecord() {
+ return mUserResourceTracker.getUserRecord(uid);
}
- /**
- * If the Binder object dies, this function is called to free the system resources that are
- * being managed by this record and to subsequently release this record for garbage
- * collection
- */
- public final void binderDied() {
- try {
- release();
- } catch (Exception e) {
- Log.e(TAG, "Failed to release resource: " + e);
- }
- }
-
- /**
- * Implement this method to release all system resources that are being protected by this
- * record. Once the resources are released, the record should be invalidated and no longer
- * used by calling release(). This should NEVER be called directly.
- *
- * <p>Calls to this are always guarded by IpSecService#this
- */
- protected abstract void releaseResources() throws RemoteException;
+ @Override
+ public abstract void freeUnderlyingResources() throws RemoteException;
/** Get the resource tracker for this resource */
protected abstract ResourceTracker getResourceTracker();
@@ -331,30 +466,52 @@ public class IpSecService extends IIpSecService.Stub {
.append(pid)
.append(", uid=")
.append(uid)
- .append(", mReferenceCount=")
- .append(mReferenceCount.get())
.append("}")
.toString();
}
};
+ // TODO: Move this to right after RefcountedResource. With this here, Gerrit was showing many
+ // more things as changed.
/**
- * Minimal wrapper around SparseArray that performs ownership validation on element accesses.
+ * Thin wrapper over SparseArray to ensure resources exist, and simplify generic typing.
+ *
+ * <p>RefcountedResourceArray prevents null insertions, and throws an IllegalArgumentException
+ * if a key is not found during a retrieval process.
*/
- private class ManagedResourceArray<T extends ManagedResource> {
- SparseArray<T> mArray = new SparseArray<>();
-
- T getAndCheckOwner(int key) {
- T val = mArray.get(key);
- // The value should never be null unless the resource doesn't exist
- // (since we do not allow null resources to be added).
- if (val != null) {
- val.checkOwnerOrSystem();
+ static class RefcountedResourceArray<T extends IResource> {
+ SparseArray<RefcountedResource<T>> mArray = new SparseArray<>();
+ private final String mTypeName;
+
+ public RefcountedResourceArray(String typeName) {
+ this.mTypeName = typeName;
+ }
+
+ /**
+ * Accessor method to get inner resource object.
+ *
+ * @throws IllegalArgumentException if no resource with provided key is found.
+ */
+ T getResourceOrThrow(int key) {
+ return getRefcountedResourceOrThrow(key).getResource();
+ }
+
+ /**
+ * Accessor method to get reference counting wrapper.
+ *
+ * @throws IllegalArgumentException if no resource with provided key is found.
+ */
+ RefcountedResource<T> getRefcountedResourceOrThrow(int key) {
+ RefcountedResource<T> resource = mArray.get(key);
+ if (resource == null) {
+ throw new IllegalArgumentException(
+ String.format("No such %s found for given id: %d", mTypeName, key));
}
- return val;
+
+ return resource;
}
- void put(int key, T obj) {
+ void put(int key, RefcountedResource<T> obj) {
checkNotNull(obj, "Null resources cannot be added");
mArray.put(key, obj);
}
@@ -369,30 +526,17 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- private final class TransformRecord extends ManagedResource {
+ private final class TransformRecord extends KernelResourceRecord {
private final IpSecConfig mConfig;
private final SpiRecord[] mSpis;
- private final UdpSocketRecord mSocket;
+ private final EncapSocketRecord mSocket;
TransformRecord(
- int resourceId,
- IBinder binder,
- IpSecConfig config,
- SpiRecord[] spis,
- UdpSocketRecord socket) {
- super(resourceId, binder);
+ int resourceId, IpSecConfig config, SpiRecord[] spis, EncapSocketRecord socket) {
+ super(resourceId);
mConfig = config;
mSpis = spis;
mSocket = socket;
-
- for (int direction : DIRECTIONS) {
- mSpis[direction].addReference();
- mSpis[direction].setOwnedByTransform();
- }
-
- if (mSocket != null) {
- mSocket.addReference();
- }
}
public IpSecConfig getConfig() {
@@ -405,7 +549,7 @@ public class IpSecService extends IIpSecService.Stub {
/** always guarded by IpSecService#this */
@Override
- protected void releaseResources() {
+ public void freeUnderlyingResources() {
for (int direction : DIRECTIONS) {
int spi = mSpis[direction].getSpi();
try {
@@ -424,17 +568,17 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- for (int direction : DIRECTIONS) {
- mSpis[direction].removeReference();
- }
+ getResourceTracker().give();
+ }
- if (mSocket != null) {
- mSocket.removeReference();
- }
+ @Override
+ public void invalidate() throws RemoteException {
+ getUserRecord().removeTransformRecord(mResourceId);
}
+ @Override
protected ResourceTracker getResourceTracker() {
- return mUserQuotaTracker.getUserRecord(this.uid).transform;
+ return getUserRecord().mTransformQuotaTracker;
}
@Override
@@ -456,7 +600,7 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- private final class SpiRecord extends ManagedResource {
+ private final class SpiRecord extends KernelResourceRecord {
private final int mDirection;
private final String mLocalAddress;
private final String mRemoteAddress;
@@ -466,12 +610,11 @@ public class IpSecService extends IIpSecService.Stub {
SpiRecord(
int resourceId,
- IBinder binder,
int direction,
String localAddress,
String remoteAddress,
int spi) {
- super(resourceId, binder);
+ super(resourceId);
mDirection = direction;
mLocalAddress = localAddress;
mRemoteAddress = remoteAddress;
@@ -480,7 +623,7 @@ public class IpSecService extends IIpSecService.Stub {
/** always guarded by IpSecService#this */
@Override
- protected void releaseResources() {
+ public void freeUnderlyingResources() {
if (mOwnedByTransform) {
Log.d(TAG, "Cannot release Spi " + mSpi + ": Currently locked by a Transform");
// Because SPIs are "handed off" to transform, objects, they should never be
@@ -503,11 +646,8 @@ public class IpSecService extends IIpSecService.Stub {
}
mSpi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
- }
- @Override
- protected ResourceTracker getResourceTracker() {
- return mUserQuotaTracker.getUserRecord(this.uid).spi;
+ getResourceTracker().give();
}
public int getSpi() {
@@ -524,6 +664,16 @@ public class IpSecService extends IIpSecService.Stub {
}
@Override
+ public void invalidate() throws RemoteException {
+ getUserRecord().removeSpiRecord(mResourceId);
+ }
+
+ @Override
+ protected ResourceTracker getResourceTracker() {
+ return getUserRecord().mSpiQuotaTracker;
+ }
+
+ @Override
public String toString() {
StringBuilder strBuilder = new StringBuilder();
strBuilder
@@ -544,27 +694,24 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- private final class UdpSocketRecord extends ManagedResource {
+ private final class EncapSocketRecord extends KernelResourceRecord {
private FileDescriptor mSocket;
private final int mPort;
- UdpSocketRecord(int resourceId, IBinder binder, FileDescriptor socket, int port) {
- super(resourceId, binder);
+ EncapSocketRecord(int resourceId, FileDescriptor socket, int port) {
+ super(resourceId);
mSocket = socket;
mPort = port;
}
/** always guarded by IpSecService#this */
@Override
- protected void releaseResources() {
+ public void freeUnderlyingResources() {
Log.d(TAG, "Closing port " + mPort);
IoUtils.closeQuietly(mSocket);
mSocket = null;
- }
- @Override
- protected ResourceTracker getResourceTracker() {
- return mUserQuotaTracker.getUserRecord(this.uid).socket;
+ getResourceTracker().give();
}
public int getPort() {
@@ -576,6 +723,16 @@ public class IpSecService extends IIpSecService.Stub {
}
@Override
+ protected ResourceTracker getResourceTracker() {
+ return getUserRecord().mSocketQuotaTracker;
+ }
+
+ @Override
+ public void invalidate() {
+ getUserRecord().removeEncapSocketRecord(mResourceId);
+ }
+
+ @Override
public String toString() {
return new StringBuilder()
.append("{super=")
@@ -607,8 +764,23 @@ public class IpSecService extends IIpSecService.Stub {
/** @hide */
@VisibleForTesting
public IpSecService(Context context, IpSecServiceConfiguration config) {
+ this(context, config, (fd, uid) -> {
+ try{
+ TrafficStats.setThreadStatsUid(uid);
+ TrafficStats.tagFileDescriptor(fd);
+ } finally {
+ TrafficStats.clearThreadStatsUid();
+ }
+ });
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public IpSecService(
+ Context context, IpSecServiceConfiguration config, UidFdTagger uidFdTagger) {
mContext = context;
mSrvConfig = config;
+ mUidFdTagger = uidFdTagger;
}
public void systemReady() {
@@ -672,23 +844,24 @@ public class IpSecService extends IIpSecService.Stub {
throw new IllegalArgumentException("Invalid Direction: " + direction);
}
- @Override
/** Get a new SPI and maintain the reservation in the system server */
- public synchronized IpSecSpiResponse reserveSecurityParameterIndex(
+ @Override
+ public synchronized IpSecSpiResponse allocateSecurityParameterIndex(
int direction, String remoteAddress, int requestedSpi, IBinder binder)
throws RemoteException {
checkDirection(direction);
checkInetAddress(remoteAddress);
/* requestedSpi can be anything in the int range, so no check is needed. */
- checkNotNull(binder, "Null Binder passed to reserveSecurityParameterIndex");
+ checkNotNull(binder, "Null Binder passed to allocateSecurityParameterIndex");
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
int resourceId = mNextResourceId.getAndIncrement();
int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
String localAddress = "";
try {
- if (!mUserQuotaTracker.getUserRecord(Binder.getCallingUid()).spi.isAvailable()) {
+ if (!userRecord.mSpiQuotaTracker.isAvailable()) {
return new IpSecSpiResponse(
IpSecManager.Status.RESOURCE_UNAVAILABLE, INVALID_RESOURCE_ID, spi);
}
@@ -702,9 +875,11 @@ public class IpSecService extends IIpSecService.Stub {
remoteAddress,
requestedSpi);
Log.d(TAG, "Allocated SPI " + spi);
- mSpiRecords.put(
+ userRecord.mSpiRecords.put(
resourceId,
- new SpiRecord(resourceId, binder, direction, localAddress, remoteAddress, spi));
+ new RefcountedResource<SpiRecord>(
+ new SpiRecord(resourceId, direction, localAddress, remoteAddress, spi),
+ binder));
} catch (ServiceSpecificException e) {
// TODO: Add appropriate checks when other ServiceSpecificException types are supported
return new IpSecSpiResponse(
@@ -718,26 +893,17 @@ public class IpSecService extends IIpSecService.Stub {
/* This method should only be called from Binder threads. Do not call this from
* within the system server as it will crash the system on failure.
*/
- private synchronized <T extends ManagedResource> void releaseManagedResource(
- ManagedResourceArray<T> resArray, int resourceId, String typeName)
+ private void releaseResource(RefcountedResourceArray resArray, int resourceId)
throws RemoteException {
- // We want to non-destructively get so that we can check credentials before removing
- // this from the records.
- T record = resArray.getAndCheckOwner(resourceId);
- if (record == null) {
- throw new IllegalArgumentException(
- typeName + " " + resourceId + " is not available to be deleted");
- }
-
- record.release();
- resArray.remove(resourceId);
+ resArray.getRefcountedResourceOrThrow(resourceId).userRelease();
}
/** Release a previously allocated SPI that has been registered with the system server */
@Override
- public void releaseSecurityParameterIndex(int resourceId) throws RemoteException {
- releaseManagedResource(mSpiRecords, resourceId, "SecurityParameterIndex");
+ public synchronized void releaseSecurityParameterIndex(int resourceId) throws RemoteException {
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+ releaseResource(userRecord.mSpiRecords, resourceId);
}
/**
@@ -776,6 +942,26 @@ public class IpSecService extends IIpSecService.Stub {
}
/**
+ * Functional interface to do traffic tagging of given sockets to UIDs.
+ *
+ * <p>Specifically used by openUdpEncapsulationSocket to ensure data usage on the UDP encap
+ * sockets are billed to the UID that the UDP encap socket was created on behalf of.
+ *
+ * <p>Separate class so that the socket tagging logic can be mocked; TrafficStats uses static
+ * methods that cannot be easily mocked/tested.
+ */
+ @VisibleForTesting
+ public interface UidFdTagger {
+ /**
+ * Sets socket tag to assign all traffic to the provided UID.
+ *
+ * <p>Since the socket is created on behalf of an unprivileged application, all traffic
+ * should be accounted to the UID of the unprivileged application.
+ */
+ public void tag(FileDescriptor fd, int uid) throws IOException;
+ }
+
+ /**
* Open a socket via the system server and bind it to the specified port (random if port=0).
* This will return a PFD to the user that represent a bound UDP socket. The system server will
* cache the socket and a record of its owner so that it can and must be freed when no longer
@@ -790,21 +976,18 @@ public class IpSecService extends IIpSecService.Stub {
}
checkNotNull(binder, "Null Binder passed to openUdpEncapsulationSocket");
+ int callingUid = Binder.getCallingUid();
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid);
int resourceId = mNextResourceId.getAndIncrement();
FileDescriptor sockFd = null;
try {
- if (!mUserQuotaTracker.getUserRecord(Binder.getCallingUid()).socket.isAvailable()) {
+ if (!userRecord.mSocketQuotaTracker.isAvailable()) {
return new IpSecUdpEncapResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
}
sockFd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+ mUidFdTagger.tag(sockFd, callingUid);
- if (port != 0) {
- Log.v(TAG, "Binding to port " + port);
- Os.bind(sockFd, INADDR_ANY, port);
- } else {
- port = bindToRandomPort(sockFd);
- }
// This code is common to both the unspecified and specified port cases
Os.setsockoptInt(
sockFd,
@@ -812,8 +995,18 @@ public class IpSecService extends IIpSecService.Stub {
OsConstants.UDP_ENCAP,
OsConstants.UDP_ENCAP_ESPINUDP);
- mUdpSocketRecords.put(
- resourceId, new UdpSocketRecord(resourceId, binder, sockFd, port));
+ mSrvConfig.getNetdInstance().ipSecSetEncapSocketOwner(sockFd, callingUid);
+ if (port != 0) {
+ Log.v(TAG, "Binding to port " + port);
+ Os.bind(sockFd, INADDR_ANY, port);
+ } else {
+ port = bindToRandomPort(sockFd);
+ }
+
+ userRecord.mEncapSocketRecords.put(
+ resourceId,
+ new RefcountedResource<EncapSocketRecord>(
+ new EncapSocketRecord(resourceId, sockFd, port), binder));
return new IpSecUdpEncapResponse(IpSecManager.Status.OK, resourceId, port, sockFd);
} catch (IOException | ErrnoException e) {
IoUtils.closeQuietly(sockFd);
@@ -825,9 +1018,9 @@ public class IpSecService extends IIpSecService.Stub {
/** close a socket that has been been allocated by and registered with the system server */
@Override
- public void closeUdpEncapsulationSocket(int resourceId) throws RemoteException {
-
- releaseManagedResource(mUdpSocketRecords, resourceId, "UdpEncapsulationSocket");
+ public synchronized void closeUdpEncapsulationSocket(int resourceId) throws RemoteException {
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+ releaseResource(userRecord.mEncapSocketRecords, resourceId);
}
/**
@@ -835,6 +1028,8 @@ public class IpSecService extends IIpSecService.Stub {
* IllegalArgumentException if they are not.
*/
private void checkIpSecConfig(IpSecConfig config) {
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
if (config.getLocalAddress() == null) {
throw new IllegalArgumentException("Invalid null Local InetAddress");
}
@@ -863,12 +1058,9 @@ public class IpSecService extends IIpSecService.Stub {
break;
case IpSecTransform.ENCAP_ESPINUDP:
case IpSecTransform.ENCAP_ESPINUDP_NON_IKE:
- if (mUdpSocketRecords.getAndCheckOwner(
- config.getEncapSocketResourceId()) == null) {
- throw new IllegalStateException(
- "No Encapsulation socket for Resource Id: "
- + config.getEncapSocketResourceId());
- }
+ // Retrieve encap socket record; will throw IllegalArgumentException if not found
+ userRecord.mEncapSocketRecords.getResourceOrThrow(
+ config.getEncapSocketResourceId());
int port = config.getEncapRemotePort();
if (port <= 0 || port > 0xFFFF) {
@@ -892,9 +1084,8 @@ public class IpSecService extends IIpSecService.Stub {
+ " exclusive with other Authentication or Encryption algorithms");
}
- if (mSpiRecords.getAndCheckOwner(config.getSpiResourceId(direction)) == null) {
- throw new IllegalStateException("No SPI for specified Resource Id");
- }
+ // Retrieve SPI record; will throw IllegalArgumentException if not found
+ userRecord.mSpiRecords.getResourceOrThrow(config.getSpiResourceId(direction));
}
}
@@ -911,16 +1102,27 @@ public class IpSecService extends IIpSecService.Stub {
checkIpSecConfig(c);
checkNotNull(binder, "Null Binder passed to createTransportModeTransform");
int resourceId = mNextResourceId.getAndIncrement();
- if (!mUserQuotaTracker.getUserRecord(Binder.getCallingUid()).transform.isAvailable()) {
+
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
+ // Avoid resizing by creating a dependency array of min-size 3 (1 UDP encap + 2 SPIs)
+ List<RefcountedResource> dependencies = new ArrayList<>(3);
+
+ if (!userRecord.mTransformQuotaTracker.isAvailable()) {
return new IpSecTransformResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
}
SpiRecord[] spis = new SpiRecord[DIRECTIONS.length];
int encapType, encapLocalPort = 0, encapRemotePort = 0;
- UdpSocketRecord socketRecord = null;
+ EncapSocketRecord socketRecord = null;
encapType = c.getEncapType();
if (encapType != IpSecTransform.ENCAP_NONE) {
- socketRecord = mUdpSocketRecords.getAndCheckOwner(c.getEncapSocketResourceId());
+ RefcountedResource<EncapSocketRecord> refcountedSocketRecord =
+ userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(
+ c.getEncapSocketResourceId());
+ dependencies.add(refcountedSocketRecord);
+
+ socketRecord = refcountedSocketRecord.getResource();
encapLocalPort = socketRecord.getPort();
encapRemotePort = c.getEncapRemotePort();
}
@@ -930,7 +1132,12 @@ public class IpSecService extends IIpSecService.Stub {
IpSecAlgorithm crypt = c.getEncryption(direction);
IpSecAlgorithm authCrypt = c.getAuthenticatedEncryption(direction);
- spis[direction] = mSpiRecords.getAndCheckOwner(c.getSpiResourceId(direction));
+ RefcountedResource<SpiRecord> refcountedSpiRecord =
+ userRecord.mSpiRecords.getRefcountedResourceOrThrow(
+ c.getSpiResourceId(direction));
+ dependencies.add(refcountedSpiRecord);
+
+ spis[direction] = refcountedSpiRecord.getResource();
int spi = spis[direction].getSpi();
try {
mSrvConfig
@@ -961,8 +1168,12 @@ public class IpSecService extends IIpSecService.Stub {
}
}
// Both SAs were created successfully, time to construct a record and lock it away
- mTransformRecords.put(
- resourceId, new TransformRecord(resourceId, binder, c, spis, socketRecord));
+ userRecord.mTransformRecords.put(
+ resourceId,
+ new RefcountedResource<TransformRecord>(
+ new TransformRecord(resourceId, c, spis, socketRecord),
+ binder,
+ dependencies.toArray(new RefcountedResource[dependencies.size()])));
return new IpSecTransformResponse(IpSecManager.Status.OK, resourceId);
}
@@ -973,8 +1184,9 @@ public class IpSecService extends IIpSecService.Stub {
* other reasons.
*/
@Override
- public void deleteTransportModeTransform(int resourceId) throws RemoteException {
- releaseManagedResource(mTransformRecords, resourceId, "IpSecTransform");
+ public synchronized void deleteTransportModeTransform(int resourceId) throws RemoteException {
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+ releaseResource(userRecord.mTransformRecords, resourceId);
}
/**
@@ -984,14 +1196,10 @@ public class IpSecService extends IIpSecService.Stub {
@Override
public synchronized void applyTransportModeTransform(
ParcelFileDescriptor socket, int resourceId) throws RemoteException {
- // Synchronize liberally here because we are using ManagedResources in this block
- TransformRecord info;
- // FIXME: this code should be factored out into a security check + getter
- info = mTransformRecords.getAndCheckOwner(resourceId);
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
- if (info == null) {
- throw new IllegalArgumentException("Transform " + resourceId + " is not active");
- }
+ // Get transform record; if no transform is found, will throw IllegalArgumentException
+ TransformRecord info = userRecord.mTransformRecords.getResourceOrThrow(resourceId);
// TODO: make this a function.
if (info.pid != getCallingPid() || info.uid != getCallingUid()) {
@@ -1023,7 +1231,7 @@ public class IpSecService extends IIpSecService.Stub {
* used: reserved for future improved input validation.
*/
@Override
- public void removeTransportModeTransform(ParcelFileDescriptor socket, int resourceId)
+ public synchronized void removeTransportModeTransform(ParcelFileDescriptor socket, int resourceId)
throws RemoteException {
try {
mSrvConfig
@@ -1042,13 +1250,7 @@ public class IpSecService extends IIpSecService.Stub {
pw.println("NetdNativeService Connection: " + (isNetdAlive() ? "alive" : "dead"));
pw.println();
- pw.println("mUserQuotaTracker:");
- pw.println(mUserQuotaTracker);
- pw.println("mTransformRecords:");
- pw.println(mTransformRecords);
- pw.println("mUdpSocketRecords:");
- pw.println(mUdpSocketRecords);
- pw.println("mSpiRecords:");
- pw.println(mSpiRecords);
+ pw.println("mUserResourceTracker:");
+ pw.println(mUserResourceTracker);
}
}
diff --git a/com/android/server/LocationManagerService.java b/com/android/server/LocationManagerService.java
index bdfccd6e..57c992fd 100644
--- a/com/android/server/LocationManagerService.java
+++ b/com/android/server/LocationManagerService.java
@@ -20,6 +20,7 @@ import android.app.ActivityManager;
import android.annotation.NonNull;
import android.util.ArrayMap;
import android.util.ArraySet;
+
import com.android.internal.content.PackageMonitor;
import com.android.internal.location.ProviderProperties;
import com.android.internal.location.ProviderRequest;
@@ -147,7 +148,7 @@ public class LocationManagerService extends ILocationManager.Stub {
private static final long HIGH_POWER_INTERVAL_MS = 5 * 60 * 1000;
private static final int FOREGROUND_IMPORTANCE_CUTOFF
- = ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+ = ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
// default background throttling interval if not overriden in settings
private static final long DEFAULT_BACKGROUND_THROTTLE_INTERVAL_MS = 30 * 60 * 1000;
@@ -239,7 +240,7 @@ public class LocationManagerService extends ILocationManager.Stub {
// current active user on the device - other users are denied location data
private int mCurrentUserId = UserHandle.USER_SYSTEM;
- private int[] mCurrentUserProfiles = new int[] { UserHandle.USER_SYSTEM };
+ private int[] mCurrentUserProfiles = new int[]{UserHandle.USER_SYSTEM};
private GnssLocationProvider.GnssSystemInfoProvider mGnssSystemInfoProvider;
@@ -253,7 +254,7 @@ public class LocationManagerService extends ILocationManager.Stub {
public LocationManagerService(Context context) {
super();
mContext = context;
- mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
+ mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
// Let the package manager query which are the default location
// providers as they get certain permissions granted by default.
@@ -370,18 +371,18 @@ public class LocationManagerService extends ILocationManager.Stub {
}
}, UserHandle.USER_ALL);
mContext.getContentResolver().registerContentObserver(
- Settings.Global.getUriFor(
- Settings.Global.LOCATION_BACKGROUND_THROTTLE_PACKAGE_WHITELIST),
- true,
- new ContentObserver(mLocationHandler) {
- @Override
- public void onChange(boolean selfChange) {
- synchronized (mLock) {
- updateBackgroundThrottlingWhitelistLocked();
- updateProvidersLocked();
+ Settings.Global.getUriFor(
+ Settings.Global.LOCATION_BACKGROUND_THROTTLE_PACKAGE_WHITELIST),
+ true,
+ new ContentObserver(mLocationHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ synchronized (mLock) {
+ updateBackgroundThrottlingWhitelistLocked();
+ updateProvidersLocked();
+ }
}
- }
- }, UserHandle.USER_ALL);
+ }, UserHandle.USER_ALL);
mPackageMonitor.register(mContext, mLocationHandler.getLooper(), true);
// listen for user change
@@ -402,7 +403,7 @@ public class LocationManagerService extends ILocationManager.Stub {
updateUserProfiles(mCurrentUserId);
} else if (Intent.ACTION_SHUTDOWN.equals(action)) {
// shutdown only if UserId indicates whole system, not just one user
- if(D) Log.d(TAG, "Shutdown received with UserId: " + getSendingUserId());
+ if (D) Log.d(TAG, "Shutdown received with UserId: " + getSendingUserId());
if (getSendingUserId() == UserHandle.USER_ALL) {
shutdownComponents();
}
@@ -416,13 +417,15 @@ public class LocationManagerService extends ILocationManager.Stub {
HashSet<String> affectedProviders = new HashSet<>(mRecordsByProvider.size());
synchronized (mLock) {
for (Entry<String, ArrayList<UpdateRecord>> entry
- : mRecordsByProvider.entrySet()) {
+ : mRecordsByProvider.entrySet()) {
String provider = entry.getKey();
for (UpdateRecord record : entry.getValue()) {
if (record.mReceiver.mIdentity.mUid == uid
- && record.mIsForegroundUid != foreground) {
- if (D) Log.d(TAG, "request from uid " + uid + " is now "
- + (foreground ? "foreground" : "background)"));
+ && record.mIsForegroundUid != foreground) {
+ if (D) {
+ Log.d(TAG, "request from uid " + uid + " is now "
+ + (foreground ? "foreground" : "background)"));
+ }
record.mIsForegroundUid = foreground;
if (!isThrottlingExemptLocked(record.mReceiver.mIdentity)) {
@@ -436,10 +439,12 @@ public class LocationManagerService extends ILocationManager.Stub {
}
for (Entry<IGnssMeasurementsListener, Identity> entry
- : mGnssMeasurementsListeners.entrySet()) {
+ : mGnssMeasurementsListeners.entrySet()) {
if (entry.getValue().mUid == uid) {
- if (D) Log.d(TAG, "gnss measurements listener from uid " + uid
- + " is now " + (foreground ? "foreground" : "background)"));
+ if (D) {
+ Log.d(TAG, "gnss measurements listener from uid " + uid
+ + " is now " + (foreground ? "foreground" : "background)"));
+ }
if (foreground || isThrottlingExemptLocked(entry.getValue())) {
mGnssMeasurementsProvider.addListener(entry.getKey());
} else {
@@ -449,11 +454,13 @@ public class LocationManagerService extends ILocationManager.Stub {
}
for (Entry<IGnssNavigationMessageListener, Identity> entry
- : mGnssNavigationMessageListeners.entrySet()) {
+ : mGnssNavigationMessageListeners.entrySet()) {
if (entry.getValue().mUid == uid) {
- if (D) Log.d(TAG, "gnss navigation message listener from uid "
- + uid + " is now "
- + (foreground ? "foreground" : "background)"));
+ if (D) {
+ Log.d(TAG, "gnss navigation message listener from uid "
+ + uid + " is now "
+ + (foreground ? "foreground" : "background)"));
+ }
if (foreground || isThrottlingExemptLocked(entry.getValue())) {
mGnssNavigationMessageProvider.addListener(entry.getKey());
} else {
@@ -477,7 +484,7 @@ public class LocationManagerService extends ILocationManager.Stub {
* support for components that do not wish to handle such event.
*/
private void shutdownComponents() {
- if(D) Log.d(TAG, "Shutting down components...");
+ if (D) Log.d(TAG, "Shutting down components...");
LocationProviderInterface gpsProvider = mProvidersByName.get(LocationManager.GPS_PROVIDER);
if (gpsProvider != null && gpsProvider.isEnabled()) {
@@ -563,8 +570,10 @@ public class LocationManagerService extends ILocationManager.Stub {
// as a proxy for coreApp="true"
if (pm.checkSignatures(systemPackageName, packageName)
!= PackageManager.SIGNATURE_MATCH) {
- if (D) Log.d(TAG, "Fallback candidate not signed the same as system: "
- + packageName);
+ if (D) {
+ Log.d(TAG, "Fallback candidate not signed the same as system: "
+ + packageName);
+ }
continue;
}
@@ -622,8 +631,10 @@ public class LocationManagerService extends ILocationManager.Stub {
ArrayList<String> providerPackageNames = new ArrayList<>();
String[] pkgs = resources.getStringArray(
com.android.internal.R.array.config_locationProviderPackageNames);
- if (D) Log.d(TAG, "certificates for location providers pulled from: " +
- Arrays.toString(pkgs));
+ if (D) {
+ Log.d(TAG, "certificates for location providers pulled from: " +
+ Arrays.toString(pkgs));
+ }
if (pkgs != null) providerPackageNames.addAll(Arrays.asList(pkgs));
ensureFallbackFusedProviderPresentLocked(providerPackageNames);
@@ -642,7 +653,7 @@ public class LocationManagerService extends ILocationManager.Stub {
mProxyProviders.add(networkProvider);
addProviderLocked(networkProvider);
} else {
- Slog.w(TAG, "no network location provider found");
+ Slog.w(TAG, "no network location provider found");
}
// bind to fused provider
@@ -671,7 +682,7 @@ public class LocationManagerService extends ILocationManager.Stub {
com.android.internal.R.array.config_locationProviderPackageNames,
mLocationHandler);
if (mGeocodeProvider == null) {
- Slog.e(TAG, "no geocoder provider found");
+ Slog.e(TAG, "no geocoder provider found");
}
// bind to fused hardware provider if supported
@@ -697,14 +708,14 @@ public class LocationManagerService extends ILocationManager.Stub {
// bind to geofence provider
GeofenceProxy provider = GeofenceProxy.createAndBind(
- mContext,com.android.internal.R.bool.config_enableGeofenceOverlay,
+ mContext, com.android.internal.R.bool.config_enableGeofenceOverlay,
com.android.internal.R.string.config_geofenceProviderPackageName,
com.android.internal.R.array.config_locationProviderPackageNames,
mLocationHandler,
mGpsGeofenceProxy,
flpHardwareProvider != null ? flpHardwareProvider.getGeofenceHardware() : null);
if (provider == null) {
- Slog.d(TAG, "Unable to bind FLP Geofence proxy.");
+ Slog.d(TAG, "Unable to bind FLP Geofence proxy.");
}
// bind to hardware activity recognition
@@ -751,6 +762,7 @@ public class LocationManagerService extends ILocationManager.Stub {
/**
* Called when the device's active user changes.
+ *
* @param userId the new active user's UserId
*/
private void switchUser(int userId) {
@@ -797,7 +809,7 @@ public class LocationManagerService extends ILocationManager.Stub {
final boolean mHideFromAppOps; // True if AppOps should not monitor this receiver.
final Object mKey;
- final HashMap<String,UpdateRecord> mUpdateRecords = new HashMap<>();
+ final HashMap<String, UpdateRecord> mUpdateRecords = new HashMap<>();
// True if app ops has started monitoring this receiver for locations.
boolean mOpMonitoring;
@@ -914,9 +926,9 @@ public class LocationManagerService extends ILocationManager.Stub {
/**
* Update AppOps monitoring for a single location request and op type.
*
- * @param allowMonitoring True if monitoring is allowed for this request/op.
+ * @param allowMonitoring True if monitoring is allowed for this request/op.
* @param currentlyMonitoring True if AppOps is currently monitoring this request/op.
- * @param op AppOps code for the op to update.
+ * @param op AppOps code for the op to update.
* @return True if monitoring is on for this request/op after updating.
*/
private boolean updateMonitoring(boolean allowMonitoring, boolean currentlyMonitoring,
@@ -1004,7 +1016,8 @@ public class LocationManagerService extends ILocationManager.Stub {
}
} else {
Intent locationChanged = new Intent();
- locationChanged.putExtra(LocationManager.KEY_LOCATION_CHANGED, new Location(location));
+ locationChanged.putExtra(LocationManager.KEY_LOCATION_CHANGED,
+ new Location(location));
try {
synchronized (this) {
// synchronize to ensure incrementPendingBroadcastsLocked()
@@ -1131,7 +1144,7 @@ public class LocationManagerService extends ILocationManager.Stub {
}
/**
- * Returns the system information of the GNSS hardware.
+ * Returns the year of the GNSS hardware.
*/
@Override
public int getGnssYearOfHardware() {
@@ -1142,6 +1155,19 @@ public class LocationManagerService extends ILocationManager.Stub {
}
}
+
+ /**
+ * Returns the model name of the GNSS hardware.
+ */
+ @Override
+ public String getGnssHardwareModelName() {
+ if (mGnssSystemInfoProvider != null) {
+ return mGnssSystemInfoProvider.getGnssHardwareModelName();
+ } else {
+ return LocationManager.GNSS_HARDWARE_MODEL_NAME_UNKNOWN;
+ }
+ }
+
/**
* Runs some checks for GNSS (FINE) level permissions, used by several methods which directly
* (try to) access GNSS information at this layer.
@@ -1286,7 +1312,7 @@ public class LocationManagerService extends ILocationManager.Stub {
}
if (mGnssBatchingProvider != null) {
- mGnssBatchingProvider.flush();
+ mGnssBatchingProvider.flush();
}
}
@@ -1301,7 +1327,7 @@ public class LocationManagerService extends ILocationManager.Stub {
if (mGnssBatchingProvider != null) {
mGnssBatchingInProgress = false;
return mGnssBatchingProvider.stop();
- } else {
+ } else {
return false;
}
}
@@ -1363,7 +1389,7 @@ public class LocationManagerService extends ILocationManager.Stub {
* processes belonging to background users.
*
* @param provider the name of the location provider
- * @param uid the requestor's UID
+ * @param uid the requestor's UID
*/
private boolean isAllowedByUserSettingsLocked(String provider, int uid) {
if (!isCurrentProfile(UserHandle.getUserId(uid)) && !isUidALocationProvider(uid)) {
@@ -1467,7 +1493,7 @@ public class LocationManagerService extends ILocationManager.Stub {
* location provider.
*
* @param allowedResolutionLevel resolution level allowed to caller
- * @param providerName the name of the location provider
+ * @param providerName the name of the location provider
*/
private void checkResolutionLevelIsSufficientForProviderUse(int allowedResolutionLevel,
String providerName) {
@@ -1718,7 +1744,9 @@ public class LocationManagerService extends ILocationManager.Stub {
resolver,
Settings.Global.LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS,
DEFAULT_BACKGROUND_THROTTLE_INTERVAL_MS);
+ // initialize the low power mode to true and set to false if any of the records requires
+ providerRequest.lowPowerMode = true;
if (records != null) {
for (UpdateRecord record : records) {
if (isCurrentProfile(UserHandle.getUserId(record.mReceiver.mIdentity.mUid))) {
@@ -1742,6 +1770,9 @@ public class LocationManagerService extends ILocationManager.Stub {
record.mRequest = locationRequest;
providerRequest.locationRequests.add(locationRequest);
+ if (!locationRequest.isLowPowerMode()) {
+ providerRequest.lowPowerMode = false;
+ }
if (interval < providerRequest.interval) {
providerRequest.reportLocation = true;
providerRequest.interval = interval;
@@ -1794,23 +1825,23 @@ public class LocationManagerService extends ILocationManager.Stub {
public String[] getBackgroundThrottlingWhitelist() {
synchronized (mLock) {
return mBackgroundThrottlePackageWhitelist.toArray(
- new String[mBackgroundThrottlePackageWhitelist.size()]);
+ new String[mBackgroundThrottlePackageWhitelist.size()]);
}
}
private void updateBackgroundThrottlingWhitelistLocked() {
String setting = Settings.Global.getString(
- mContext.getContentResolver(),
- Settings.Global.LOCATION_BACKGROUND_THROTTLE_PACKAGE_WHITELIST);
+ mContext.getContentResolver(),
+ Settings.Global.LOCATION_BACKGROUND_THROTTLE_PACKAGE_WHITELIST);
if (setting == null) {
setting = "";
}
mBackgroundThrottlePackageWhitelist.clear();
mBackgroundThrottlePackageWhitelist.addAll(
- SystemConfig.getInstance().getAllowUnthrottledLocation());
+ SystemConfig.getInstance().getAllowUnthrottledLocation());
mBackgroundThrottlePackageWhitelist.addAll(
- Arrays.asList(setting.split(",")));
+ Arrays.asList(setting.split(",")));
}
private boolean isThrottlingExemptLocked(Identity identity) {
@@ -1894,7 +1925,8 @@ public class LocationManagerService extends ILocationManager.Stub {
@Override
public String toString() {
return "UpdateRecord[" + mProvider + " " + mReceiver.mIdentity.mPackageName
- + "(" + mReceiver.mIdentity.mUid + (mIsForegroundUid ? " foreground" : " background")
+ + "(" + mReceiver.mIdentity.mUid + (mIsForegroundUid ? " foreground"
+ : " background")
+ ")" + " " + mRealRequest + "]";
}
}
@@ -1936,8 +1968,13 @@ public class LocationManagerService extends ILocationManager.Stub {
* @return a version of request that meets the given resolution and consistency requirements
* @hide
*/
- private LocationRequest createSanitizedRequest(LocationRequest request, int resolutionLevel) {
+ private LocationRequest createSanitizedRequest(LocationRequest request, int resolutionLevel,
+ boolean callerHasLocationHardwarePermission) {
LocationRequest sanitizedRequest = new LocationRequest(request);
+ if (!callerHasLocationHardwarePermission) {
+ // allow setting low power mode only for callers with location hardware permission
+ sanitizedRequest.setLowPowerMode(false);
+ }
if (resolutionLevel < RESOLUTION_LEVEL_FINE) {
switch (sanitizedRequest.getQuality()) {
case LocationRequest.ACCURACY_FINE:
@@ -2013,7 +2050,11 @@ public class LocationManagerService extends ILocationManager.Stub {
if (hideFromAppOps) {
checkUpdateAppOpsAllowed();
}
- LocationRequest sanitizedRequest = createSanitizedRequest(request, allowedResolutionLevel);
+ boolean callerHasLocationHardwarePermission =
+ mContext.checkCallingPermission(android.Manifest.permission.LOCATION_HARDWARE)
+ == PackageManager.PERMISSION_GRANTED;
+ LocationRequest sanitizedRequest = createSanitizedRequest(request, allowedResolutionLevel,
+ callerHasLocationHardwarePermission);
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();
@@ -2050,11 +2091,13 @@ public class LocationManagerService extends ILocationManager.Stub {
}
UpdateRecord record = new UpdateRecord(name, request, receiver);
- if (D) Log.d(TAG, "request " + Integer.toHexString(System.identityHashCode(receiver))
- + " " + name + " " + request + " from " + packageName + "(" + uid + " "
- + (record.mIsForegroundUid ? "foreground" : "background")
- + (isThrottlingExemptLocked(receiver.mIdentity)
+ if (D) {
+ Log.d(TAG, "request " + Integer.toHexString(System.identityHashCode(receiver))
+ + " " + name + " " + request + " from " + packageName + "(" + uid + " "
+ + (record.mIsForegroundUid ? "foreground" : "background")
+ + (isThrottlingExemptLocked(receiver.mIdentity)
? " [whitelisted]" : "") + ")");
+ }
UpdateRecord oldRecord = receiver.mUpdateRecords.put(name, record);
if (oldRecord != null) {
@@ -2159,14 +2202,18 @@ public class LocationManagerService extends ILocationManager.Stub {
final long identity = Binder.clearCallingIdentity();
try {
if (mBlacklist.isBlacklisted(packageName)) {
- if (D) Log.d(TAG, "not returning last loc for blacklisted app: " +
- packageName);
+ if (D) {
+ Log.d(TAG, "not returning last loc for blacklisted app: " +
+ packageName);
+ }
return null;
}
if (!reportLocationAccessNoThrow(pid, uid, packageName, allowedResolutionLevel)) {
- if (D) Log.d(TAG, "not returning last loc for no op app: " +
- packageName);
+ if (D) {
+ Log.d(TAG, "not returning last loc for no op app: " +
+ packageName);
+ }
return null;
}
@@ -2192,7 +2239,8 @@ public class LocationManagerService extends ILocationManager.Stub {
return null;
}
if (allowedResolutionLevel < RESOLUTION_LEVEL_FINE) {
- Location noGPSLocation = location.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION);
+ Location noGPSLocation = location.getExtraLocation(
+ Location.EXTRA_NO_GPS_LOCATION);
if (noGPSLocation != null) {
return new Location(mLocationFudger.getOrCreate(noGPSLocation));
}
@@ -2216,7 +2264,12 @@ public class LocationManagerService extends ILocationManager.Stub {
checkPackageName(packageName);
checkResolutionLevelIsSufficientForProviderUse(allowedResolutionLevel,
request.getProvider());
- LocationRequest sanitizedRequest = createSanitizedRequest(request, allowedResolutionLevel);
+ // Require that caller can manage given document
+ boolean callerHasLocationHardwarePermission =
+ mContext.checkCallingPermission(android.Manifest.permission.LOCATION_HARDWARE)
+ == PackageManager.PERMISSION_GRANTED;
+ LocationRequest sanitizedRequest = createSanitizedRequest(request, allowedResolutionLevel,
+ callerHasLocationHardwarePermission);
if (D) Log.d(TAG, "requestGeofence: " + sanitizedRequest + " " + geofence + " " + intent);
@@ -2282,8 +2335,7 @@ public class LocationManagerService extends ILocationManager.Stub {
@Override
public boolean addGnssMeasurementsListener(
- IGnssMeasurementsListener listener,
- String packageName) {
+ IGnssMeasurementsListener listener, String packageName) {
if (!hasGnssPermissions(packageName) || mGnssMeasurementsProvider == null) {
return false;
}
@@ -2296,7 +2348,7 @@ public class LocationManagerService extends ILocationManager.Stub {
try {
if (isThrottlingExemptLocked(callerIdentity)
|| isImportanceForeground(
- mActivityManager.getPackageImportance(packageName))) {
+ mActivityManager.getPackageImportance(packageName))) {
return mGnssMeasurementsProvider.addListener(listener);
}
} finally {
@@ -2327,13 +2379,13 @@ public class LocationManagerService extends ILocationManager.Stub {
synchronized (mLock) {
Identity callerIdentity
- = new Identity(Binder.getCallingUid(), Binder.getCallingPid(), packageName);
+ = new Identity(Binder.getCallingUid(), Binder.getCallingPid(), packageName);
mGnssNavigationMessageListeners.put(listener, callerIdentity);
long identity = Binder.clearCallingIdentity();
try {
if (isThrottlingExemptLocked(callerIdentity)
|| isImportanceForeground(
- mActivityManager.getPackageImportance(packageName))) {
+ mActivityManager.getPackageImportance(packageName))) {
return mGnssNavigationMessageProvider.addListener(listener);
}
} finally {
@@ -2394,7 +2446,7 @@ public class LocationManagerService extends ILocationManager.Stub {
/**
* @return null if the provider does not exist
* @throws SecurityException if the provider is not allowed to be
- * accessed by the caller
+ * accessed by the caller
*/
@Override
public ProviderProperties getProviderProperties(String provider) {
@@ -2417,7 +2469,7 @@ public class LocationManagerService extends ILocationManager.Stub {
/**
* @return null if the provider does not exist
* @throws SecurityException if the provider is not allowed to be
- * accessed by the caller
+ * accessed by the caller
*/
@Override
public String getNetworkProviderPackage() {
@@ -2641,8 +2693,10 @@ public class LocationManagerService extends ILocationManager.Stub {
}
if (mBlacklist.isBlacklisted(receiver.mIdentity.mPackageName)) {
- if (D) Log.d(TAG, "skipping loc update for blacklisted app: " +
- receiver.mIdentity.mPackageName);
+ if (D) {
+ Log.d(TAG, "skipping loc update for blacklisted app: " +
+ receiver.mIdentity.mPackageName);
+ }
continue;
}
@@ -2651,8 +2705,10 @@ public class LocationManagerService extends ILocationManager.Stub {
receiver.mIdentity.mUid,
receiver.mIdentity.mPackageName,
receiver.mAllowedResolutionLevel)) {
- if (D) Log.d(TAG, "skipping loc update for no op app: " +
- receiver.mIdentity.mPackageName);
+ if (D) {
+ Log.d(TAG, "skipping loc update for no op app: " +
+ receiver.mIdentity.mPackageName);
+ }
continue;
}
@@ -3114,12 +3170,12 @@ public class LocationManagerService extends ILocationManager.Stub {
}
pw.append(" fudger: ");
- mLocationFudger.dump(fd, pw, args);
+ mLocationFudger.dump(fd, pw, args);
if (args.length > 0 && "short".equals(args[0])) {
return;
}
- for (LocationProviderInterface provider: mProviders) {
+ for (LocationProviderInterface provider : mProviders) {
pw.print(provider.getName() + " Internal State");
if (provider instanceof LocationProviderProxy) {
LocationProviderProxy proxy = (LocationProviderProxy) provider;
diff --git a/com/android/server/ServiceWatcher.java b/com/android/server/ServiceWatcher.java
index f20ca435..f4238f21 100644
--- a/com/android/server/ServiceWatcher.java
+++ b/com/android/server/ServiceWatcher.java
@@ -282,6 +282,7 @@ public class ServiceWatcher implements ServiceConnection {
mBoundUserId = UserHandle.USER_NULL;
if (component != null) {
if (D) Log.d(mTag, "unbinding " + component);
+ mBoundService = null;
mContext.unbindService(this);
}
}
diff --git a/com/android/server/StorageManagerService.java b/com/android/server/StorageManagerService.java
index 66b3adb6..6a0d3ff9 100644
--- a/com/android/server/StorageManagerService.java
+++ b/com/android/server/StorageManagerService.java
@@ -31,8 +31,11 @@ import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.Manifest;
import android.annotation.Nullable;
import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.ActivityManagerInternal.ScreenObserver;
import android.app.AppOpsManager;
import android.app.IActivityManager;
+import android.app.KeyguardManager;
import android.app.usage.StorageStatsManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -160,7 +163,8 @@ import javax.crypto.spec.PBEKeySpec;
* watch for and manage dynamically added storage, such as SD cards and USB mass
* storage. Also decides how storage should be presented to users on the device.
*/
-class StorageManagerService extends IStorageManager.Stub implements Watchdog.Monitor {
+class StorageManagerService extends IStorageManager.Stub
+ implements Watchdog.Monitor, ScreenObserver {
// Static direct instance pointer for the tightly-coupled idle service to use
static StorageManagerService sSelf = null;
@@ -402,6 +406,7 @@ class StorageManagerService extends IStorageManager.Stub implements Watchdog.Mon
private volatile boolean mSystemReady = false;
private volatile boolean mBootCompleted = false;
private volatile boolean mDaemonConnected = false;
+ private volatile boolean mSecureKeyguardShowing = true;
private PackageManagerService mPms;
@@ -827,6 +832,7 @@ class StorageManagerService extends IStorageManager.Stub implements Watchdog.Mon
mVold.onUserStarted(userId);
mStoraged.onUserStarted(userId);
}
+ mVold.onSecureKeyguardStateChanged(mSecureKeyguardShowing);
} catch (Exception e) {
Slog.wtf(TAG, e);
}
@@ -878,6 +884,24 @@ class StorageManagerService extends IStorageManager.Stub implements Watchdog.Mon
}
}
+ @Override
+ public void onAwakeStateChanged(boolean isAwake) {
+ // Ignored
+ }
+
+ @Override
+ public void onKeyguardStateChanged(boolean isShowing) {
+ // Push down current secure keyguard status so that we ignore malicious
+ // USB devices while locked.
+ mSecureKeyguardShowing = isShowing
+ && mContext.getSystemService(KeyguardManager.class).isDeviceSecure();
+ try {
+ mVold.onSecureKeyguardStateChanged(mSecureKeyguardShowing);
+ } catch (Exception e) {
+ Slog.wtf(TAG, e);
+ }
+ }
+
void runIdleMaintenance(Runnable callback) {
mHandler.sendMessage(mHandler.obtainMessage(H_FSTRIM, callback));
}
@@ -1414,6 +1438,9 @@ class StorageManagerService extends IStorageManager.Stub implements Watchdog.Mon
}
private void systemReady() {
+ LocalServices.getService(ActivityManagerInternal.class)
+ .registerScreenObserver(this);
+
mSystemReady = true;
mHandler.obtainMessage(H_SYSTEM_READY).sendToTarget();
}
@@ -2611,6 +2638,7 @@ class StorageManagerService extends IStorageManager.Stub implements Watchdog.Mon
try {
mVold.mkdirs(appPath);
+ return;
} catch (Exception e) {
throw new IllegalStateException("Failed to prepare " + appPath + ": " + e);
}
@@ -2690,15 +2718,14 @@ class StorageManagerService extends IStorageManager.Stub implements Watchdog.Mon
final boolean primary = true;
final boolean removable = primaryPhysical;
final boolean emulated = !primaryPhysical;
- final long mtpReserveSize = 0L;
final boolean allowMassStorage = false;
final long maxFileSize = 0L;
final UserHandle owner = new UserHandle(userId);
final String uuid = null;
final String state = Environment.MEDIA_REMOVED;
- res.add(0, new StorageVolume(id, StorageVolume.STORAGE_ID_INVALID, path,
- description, primary, removable, emulated, mtpReserveSize,
+ res.add(0, new StorageVolume(id, path,
+ description, primary, removable, emulated,
allowMassStorage, maxFileSize, owner, uuid, state));
}
diff --git a/com/android/server/SystemConfig.java b/com/android/server/SystemConfig.java
index b5031f23..b7a67192 100644
--- a/com/android/server/SystemConfig.java
+++ b/com/android/server/SystemConfig.java
@@ -146,6 +146,9 @@ public class SystemConfig {
final ArrayMap<String, ArraySet<String>> mPrivAppPermissions = new ArrayMap<>();
final ArrayMap<String, ArraySet<String>> mPrivAppDenyPermissions = new ArrayMap<>();
+ final ArrayMap<String, ArraySet<String>> mVendorPrivAppPermissions = new ArrayMap<>();
+ final ArrayMap<String, ArraySet<String>> mVendorPrivAppDenyPermissions = new ArrayMap<>();
+
final ArrayMap<String, ArrayMap<String, Boolean>> mOemPermissions = new ArrayMap<>();
public static SystemConfig getInstance() {
@@ -229,6 +232,14 @@ public class SystemConfig {
return mPrivAppDenyPermissions.get(packageName);
}
+ public ArraySet<String> getVendorPrivAppPermissions(String packageName) {
+ return mVendorPrivAppPermissions.get(packageName);
+ }
+
+ public ArraySet<String> getVendorPrivAppDenyPermissions(String packageName) {
+ return mVendorPrivAppDenyPermissions.get(packageName);
+ }
+
public Map<String, Boolean> getOemPermissions(String packageName) {
final Map<String, Boolean> oemPermissions = mOemPermissions.get(packageName);
if (oemPermissions != null) {
@@ -248,7 +259,7 @@ public class SystemConfig {
// Allow Vendor to customize system configs around libs, features, permissions and apps
int vendorPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_PERMISSIONS |
- ALLOW_APP_CONFIGS;
+ ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS;
readPermissions(Environment.buildPath(
Environment.getVendorDirectory(), "etc", "sysconfig"), vendorPermissionFlag);
readPermissions(Environment.buildPath(
@@ -587,7 +598,19 @@ public class SystemConfig {
}
XmlUtils.skipCurrentTag(parser);
} else if ("privapp-permissions".equals(name) && allowPrivappPermissions) {
- readPrivAppPermissions(parser);
+ // privapp permissions from system and vendor partitions are stored
+ // separately. This is to prevent xml files in the vendor partition from
+ // granting permissions to priv apps in the system partition and vice
+ // versa.
+ boolean vendor = permFile.toPath().startsWith(
+ Environment.getVendorDirectory().toPath());
+ if (vendor) {
+ readPrivAppPermissions(parser, mVendorPrivAppPermissions,
+ mVendorPrivAppDenyPermissions);
+ } else {
+ readPrivAppPermissions(parser, mPrivAppPermissions,
+ mPrivAppDenyPermissions);
+ }
} else if ("oem-permissions".equals(name) && allowOemPermissions) {
readOemPermissions(parser);
} else {
@@ -674,7 +697,10 @@ public class SystemConfig {
}
}
- void readPrivAppPermissions(XmlPullParser parser) throws IOException, XmlPullParserException {
+ private void readPrivAppPermissions(XmlPullParser parser,
+ ArrayMap<String, ArraySet<String>> grantMap,
+ ArrayMap<String, ArraySet<String>> denyMap)
+ throws IOException, XmlPullParserException {
String packageName = parser.getAttributeValue(null, "package");
if (TextUtils.isEmpty(packageName)) {
Slog.w(TAG, "package is required for <privapp-permissions> in "
@@ -682,11 +708,11 @@ public class SystemConfig {
return;
}
- ArraySet<String> permissions = mPrivAppPermissions.get(packageName);
+ ArraySet<String> permissions = grantMap.get(packageName);
if (permissions == null) {
permissions = new ArraySet<>();
}
- ArraySet<String> denyPermissions = mPrivAppDenyPermissions.get(packageName);
+ ArraySet<String> denyPermissions = denyMap.get(packageName);
int depth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, depth)) {
String name = parser.getName();
@@ -711,9 +737,9 @@ public class SystemConfig {
denyPermissions.add(permName);
}
}
- mPrivAppPermissions.put(packageName, permissions);
+ grantMap.put(packageName, permissions);
if (denyPermissions != null) {
- mPrivAppDenyPermissions.put(packageName, denyPermissions);
+ denyMap.put(packageName, denyPermissions);
}
}
diff --git a/com/android/server/SystemServer.java b/com/android/server/SystemServer.java
index 33f4e348..4310a98d 100644
--- a/com/android/server/SystemServer.java
+++ b/com/android/server/SystemServer.java
@@ -26,6 +26,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources.Theme;
+import android.database.sqlite.SQLiteCompatibilityWalFlags;
import android.os.BaseBundle;
import android.os.Binder;
import android.os.Build;
@@ -215,6 +216,9 @@ public final class SystemServer {
"com.android.server.timezone.RulesManagerService$Lifecycle";
private static final String IOT_SERVICE_CLASS =
"com.google.android.things.services.IoTSystemService";
+ private static final String SLICE_MANAGER_SERVICE_CLASS =
+ "com.android.server.slice.SliceManagerService$Lifecycle";
+
private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst";
private static final String UNCRYPT_PACKAGE_FILE = "/cache/recovery/uncrypt_file";
@@ -324,6 +328,8 @@ public final class SystemServer {
// The system server should never make non-oneway calls
Binder.setWarnOnBlocking(true);
+ // Deactivate SQLiteCompatibilityWalFlags until settings provider is initialized
+ SQLiteCompatibilityWalFlags.init(null);
// Here we go!
Slog.i(TAG, "Entered the Android system server!");
@@ -730,6 +736,7 @@ public final class SystemServer {
boolean disableVrManager = SystemProperties.getBoolean("config.disable_vrmanager", false);
boolean disableCameraService = SystemProperties.getBoolean("config.disable_cameraservice",
false);
+ boolean disableSlices = SystemProperties.getBoolean("config.disable_slices", false);
boolean enableLeftyService = SystemProperties.getBoolean("config.enable_lefty", false);
boolean isEmulator = SystemProperties.get("ro.kernel.qemu").equals("1");
@@ -799,6 +806,8 @@ public final class SystemServer {
traceBeginAndSlog("InstallSystemProviders");
mActivityManagerService.installSystemProviders();
+ // Now that SettingsProvider is ready, reactivate SQLiteCompatibilityWalFlags
+ SQLiteCompatibilityWalFlags.reset();
traceEnd();
traceBeginAndSlog("StartVibratorService");
@@ -1084,14 +1093,17 @@ public final class SystemServer {
}
traceEnd();
- // Wifi Service must be started first for wifi-related services.
- traceBeginAndSlog("StartWifi");
- mSystemServiceManager.startService(WIFI_SERVICE_CLASS);
- traceEnd();
- traceBeginAndSlog("StartWifiScanning");
- mSystemServiceManager.startService(
- "com.android.server.wifi.scanner.WifiScanningService");
- traceEnd();
+ if (context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_WIFI)) {
+ // Wifi Service must be started first for wifi-related services.
+ traceBeginAndSlog("StartWifi");
+ mSystemServiceManager.startService(WIFI_SERVICE_CLASS);
+ traceEnd();
+ traceBeginAndSlog("StartWifiScanning");
+ mSystemServiceManager.startService(
+ "com.android.server.wifi.scanner.WifiScanningService");
+ traceEnd();
+ }
if (!disableRtt) {
traceBeginAndSlog("StartWifiRtt");
@@ -1523,6 +1535,12 @@ public final class SystemServer {
}
}
+ if (!disableSlices) {
+ traceBeginAndSlog("StartSliceManagerService");
+ mSystemServiceManager.startService(SLICE_MANAGER_SERVICE_CLASS);
+ traceEnd();
+ }
+
if (!disableCameraService) {
traceBeginAndSlog("StartCameraServiceProxy");
mSystemServiceManager.startService(CameraServiceProxy.class);
diff --git a/com/android/server/UiThread.java b/com/android/server/UiThread.java
index fd88d260..f8130742 100644
--- a/com/android/server/UiThread.java
+++ b/com/android/server/UiThread.java
@@ -47,7 +47,7 @@ public final class UiThread extends ServiceThread {
sInstance = new UiThread();
sInstance.start();
final Looper looper = sInstance.getLooper();
- looper.setTraceTag(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ looper.setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER);
looper.setSlowDispatchThresholdMs(SLOW_DISPATCH_THRESHOLD_MS);
sHandler = new Handler(sInstance.getLooper());
}
diff --git a/com/android/server/accessibility/AccessibilityClientConnection.java b/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 22d922be..ed068b93 100644
--- a/com/android/server/accessibility/AccessibilityClientConnection.java
+++ b/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -30,6 +30,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.graphics.Region;
import android.os.Binder;
@@ -55,6 +56,7 @@ import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.DumpUtils;
+import com.android.server.accessibility.AccessibilityManagerService.RemoteAccessibilityConnection;
import com.android.server.accessibility.AccessibilityManagerService.SecurityPolicy;
import com.android.server.wm.WindowManagerInternal;
@@ -62,7 +64,6 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -71,7 +72,7 @@ import java.util.Set;
* This class represents an accessibility client - either an AccessibilityService or a UiAutomation.
* It is responsible for behavior common to both types of clients.
*/
-abstract class AccessibilityClientConnection extends IAccessibilityServiceConnection.Stub
+abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServiceConnection.Stub
implements ServiceConnection, IBinder.DeathRecipient, KeyEventDispatcher.KeyEventFilter,
FingerprintGestureDispatcher.FingerprintGestureClient {
private static final boolean DEBUG = false;
@@ -104,7 +105,7 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
int mFeedbackType;
- final Set<String> mPackageNames = new HashSet<>();
+ Set<String> mPackageNames = new HashSet<>();
boolean mIsDefault;
@@ -169,13 +170,13 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
@NonNull MagnificationController getMagnificationController();
/**
- * Resolve a connection for a window id
+ * Resolve a connection wrapper for a window id
*
* @param windowId The id of the window of interest
*
* @return a connection to the window
*/
- IAccessibilityInteractionConnection getConnectionLocked(int windowId);
+ RemoteAccessibilityConnection getConnectionLocked(int windowId);
/**
* Perform the specified accessibility action
@@ -238,7 +239,7 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
int flags);
}
- public AccessibilityClientConnection(Context context, ComponentName componentName,
+ public AbstractAccessibilityServiceConnection(Context context, ComponentName componentName,
AccessibilityServiceInfo accessibilityServiceInfo, int id, Handler mainHandler,
Object lock, SecurityPolicy securityPolicy, SystemSupport systemSupport,
WindowManagerInternal windowManagerInternal,
@@ -282,98 +283,40 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
return true;
}
- boolean setDynamicallyConfigurableProperties(AccessibilityServiceInfo info) {
- boolean somethingChanged = false;
-
- if (mEventTypes != info.eventTypes) {
- mEventTypes = info.eventTypes;
- somethingChanged = true;
- }
-
- if (mFeedbackType != info.feedbackType) {
- mFeedbackType = info.feedbackType;
- somethingChanged = true;
+ public void setDynamicallyConfigurableProperties(AccessibilityServiceInfo info) {
+ mEventTypes = info.eventTypes;
+ mFeedbackType = info.feedbackType;
+ String[] packageNames = info.packageNames;
+ if (packageNames != null) {
+ mPackageNames.addAll(Arrays.asList(packageNames));
}
+ mNotificationTimeout = info.notificationTimeout;
+ mIsDefault = (info.flags & DEFAULT) != 0;
- final String[] oldPackageNames = mPackageNames.toArray(new String[mPackageNames.size()]);
- if (!Arrays.equals(oldPackageNames, info.packageNames)) {
- mPackageNames.clear();
- if (info.packageNames != null) {
- Collections.addAll(mPackageNames, info.packageNames);
+ if (supportsFlagForNotImportantViews(info)) {
+ if ((info.flags & AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0) {
+ mFetchFlags |= AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+ } else {
+ mFetchFlags &= ~AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
}
- somethingChanged = true;
}
- if (mNotificationTimeout != info.notificationTimeout) {
- mNotificationTimeout = info.notificationTimeout;
- somethingChanged = true;
- }
-
- final boolean newIsDefault = (info.flags & DEFAULT) != 0;
- if (mIsDefault != newIsDefault) {
- mIsDefault = newIsDefault;
- somethingChanged = true;
- }
-
- if (supportsFlagForNotImportantViews(info)) {
- somethingChanged |= updateFetchFlag(info.flags,
- AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS);
+ if ((info.flags & AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS) != 0) {
+ mFetchFlags |= AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
+ } else {
+ mFetchFlags &= ~AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS;
}
- somethingChanged |= updateFetchFlag(info.flags,
- AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS);
-
- final boolean newRequestTouchExplorationMode = (info.flags
+ mRequestTouchExplorationMode = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_TOUCH_EXPLORATION_MODE) != 0;
- if (mRequestTouchExplorationMode != newRequestTouchExplorationMode) {
- mRequestTouchExplorationMode = newRequestTouchExplorationMode;
- somethingChanged = true;
- }
-
- final boolean newRequestFilterKeyEvents = (info.flags
+ mRequestFilterKeyEvents = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS) != 0;
- if (mRequestFilterKeyEvents != newRequestFilterKeyEvents) {
- mRequestFilterKeyEvents = newRequestFilterKeyEvents;
- somethingChanged = true;
- }
-
- final boolean newRetrieveInteractiveWindows = (info.flags
+ mRetrieveInteractiveWindows = (info.flags
& AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS) != 0;
- if (mRetrieveInteractiveWindows != newRetrieveInteractiveWindows) {
- mRetrieveInteractiveWindows = newRetrieveInteractiveWindows;
- somethingChanged = true;
- }
-
- final boolean newCaptureFingerprintGestures = (info.flags
+ mCaptureFingerprintGestures = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_FINGERPRINT_GESTURES) != 0;
- if (mCaptureFingerprintGestures != newCaptureFingerprintGestures) {
- mCaptureFingerprintGestures = newCaptureFingerprintGestures;
- somethingChanged = true;
- }
-
- final boolean newRequestAccessibilityButton = (info.flags
+ mRequestAccessibilityButton = (info.flags
& AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
- if (mRequestAccessibilityButton != newRequestAccessibilityButton) {
- mRequestAccessibilityButton = newRequestAccessibilityButton;
- somethingChanged = true;
- }
-
- return somethingChanged;
- }
-
- private boolean updateFetchFlag(int allFlags, int flagToUpdate) {
- if ((allFlags & flagToUpdate) != 0) {
- if ((mFetchFlags & flagToUpdate) == 0) {
- mFetchFlags |= flagToUpdate;
- return true;
- }
- } else {
- if ((mFetchFlags & flagToUpdate) != 0) {
- mFetchFlags &= ~flagToUpdate;
- return true;
- }
- }
- return false;
}
protected boolean supportsFlagForNotImportantViews(AccessibilityServiceInfo info) {
@@ -397,6 +340,11 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
}
}
+ int getRelevantEventTypes() {
+ return (mUsesAccessibilityCache ? AccessibilityCache.CACHE_CRITICAL_EVENTS_MASK : 0)
+ | mEventTypes;
+ }
+
@Override
public void setServiceInfo(AccessibilityServiceInfo info) {
final long identity = Binder.clearCallingIdentity();
@@ -405,15 +353,14 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
// If the XML manifest had data to configure the service its info
// should be already set. In such a case update only the dynamically
// configurable properties.
- final boolean serviceInfoChanged;
AccessibilityServiceInfo oldInfo = mAccessibilityServiceInfo;
if (oldInfo != null) {
oldInfo.updateDynamicallyConfigurableProperties(info);
- serviceInfoChanged = setDynamicallyConfigurableProperties(oldInfo);
+ setDynamicallyConfigurableProperties(oldInfo);
} else {
- serviceInfoChanged = setDynamicallyConfigurableProperties(info);
+ setDynamicallyConfigurableProperties(info);
}
- mSystemSupport.onClientChange(serviceInfoChanged);
+ mSystemSupport.onClientChange(true);
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -473,28 +420,28 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
}
@Override
- public boolean findAccessibilityNodeInfosByViewId(int accessibilityWindowId,
+ public String[] findAccessibilityNodeInfosByViewId(int accessibilityWindowId,
long accessibilityNodeId, String viewIdResName, int interactionId,
IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
throws RemoteException {
final int resolvedWindowId;
- IAccessibilityInteractionConnection connection = null;
+ RemoteAccessibilityConnection connection;
Region partialInteractiveRegion = Region.obtain();
MagnificationSpec spec;
synchronized (mLock) {
mUsesAccessibilityCache = true;
if (!isCalledForCurrentUserLocked()) {
- return false;
+ return null;
}
resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
final boolean permissionGranted =
mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
if (!permissionGranted) {
- return false;
+ return null;
} else {
connection = mSystemSupport.getConnectionLocked(resolvedWindowId);
if (connection == null) {
- return false;
+ return null;
}
}
if (!mSecurityPolicy.computePartialInteractiveRegionForWindowLocked(
@@ -507,12 +454,14 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
final int interrogatingPid = Binder.getCallingPid();
callback = mSystemSupport.replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
interrogatingPid, interrogatingTid);
+ final int callingUid = Binder.getCallingUid();
final long identityToken = Binder.clearCallingIdentity();
try {
- connection.findAccessibilityNodeInfosByViewId(accessibilityNodeId, viewIdResName,
- partialInteractiveRegion, interactionId, callback, mFetchFlags,
+ connection.getRemote().findAccessibilityNodeInfosByViewId(accessibilityNodeId,
+ viewIdResName, partialInteractiveRegion, interactionId, callback, mFetchFlags,
interrogatingPid, interrogatingTid, spec);
- return true;
+ return mSecurityPolicy.computeValidReportedPackages(callingUid,
+ connection.getPackageName(), connection.getUid());
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error findAccessibilityNodeInfoByViewId().");
@@ -520,36 +469,36 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
} finally {
Binder.restoreCallingIdentity(identityToken);
// Recycle if passed to another process.
- if (partialInteractiveRegion != null && Binder.isProxy(connection)) {
+ if (partialInteractiveRegion != null && Binder.isProxy(connection.getRemote())) {
partialInteractiveRegion.recycle();
}
}
- return false;
+ return null;
}
@Override
- public boolean findAccessibilityNodeInfosByText(int accessibilityWindowId,
+ public String[] findAccessibilityNodeInfosByText(int accessibilityWindowId,
long accessibilityNodeId, String text, int interactionId,
IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
throws RemoteException {
final int resolvedWindowId;
- IAccessibilityInteractionConnection connection = null;
+ RemoteAccessibilityConnection connection;
Region partialInteractiveRegion = Region.obtain();
MagnificationSpec spec;
synchronized (mLock) {
mUsesAccessibilityCache = true;
if (!isCalledForCurrentUserLocked()) {
- return false;
+ return null;
}
resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
final boolean permissionGranted =
mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
if (!permissionGranted) {
- return false;
+ return null;
} else {
connection = mSystemSupport.getConnectionLocked(resolvedWindowId);
if (connection == null) {
- return false;
+ return null;
}
}
if (!mSecurityPolicy.computePartialInteractiveRegionForWindowLocked(
@@ -562,12 +511,14 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
final int interrogatingPid = Binder.getCallingPid();
callback = mSystemSupport.replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
interrogatingPid, interrogatingTid);
+ final int callingUid = Binder.getCallingUid();
final long identityToken = Binder.clearCallingIdentity();
try {
- connection.findAccessibilityNodeInfosByText(accessibilityNodeId, text,
- partialInteractiveRegion, interactionId, callback, mFetchFlags,
+ connection.getRemote().findAccessibilityNodeInfosByText(accessibilityNodeId,
+ text, partialInteractiveRegion, interactionId, callback, mFetchFlags,
interrogatingPid, interrogatingTid, spec);
- return true;
+ return mSecurityPolicy.computeValidReportedPackages(callingUid,
+ connection.getPackageName(), connection.getUid());
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfosByText()");
@@ -575,36 +526,36 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
} finally {
Binder.restoreCallingIdentity(identityToken);
// Recycle if passed to another process.
- if (partialInteractiveRegion != null && Binder.isProxy(connection)) {
+ if (partialInteractiveRegion != null && Binder.isProxy(connection.getRemote())) {
partialInteractiveRegion.recycle();
}
}
- return false;
+ return null;
}
@Override
- public boolean findAccessibilityNodeInfoByAccessibilityId(
+ public String[] findAccessibilityNodeInfoByAccessibilityId(
int accessibilityWindowId, long accessibilityNodeId, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int flags,
long interrogatingTid, Bundle arguments) throws RemoteException {
final int resolvedWindowId;
- IAccessibilityInteractionConnection connection = null;
+ RemoteAccessibilityConnection connection;
Region partialInteractiveRegion = Region.obtain();
MagnificationSpec spec;
synchronized (mLock) {
mUsesAccessibilityCache = true;
if (!isCalledForCurrentUserLocked()) {
- return false;
+ return null;
}
resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
final boolean permissionGranted =
mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
if (!permissionGranted) {
- return false;
+ return null;
} else {
connection = mSystemSupport.getConnectionLocked(resolvedWindowId);
if (connection == null) {
- return false;
+ return null;
}
}
if (!mSecurityPolicy.computePartialInteractiveRegionForWindowLocked(
@@ -617,12 +568,14 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
final int interrogatingPid = Binder.getCallingPid();
callback = mSystemSupport.replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
interrogatingPid, interrogatingTid);
+ final int callingUid = Binder.getCallingUid();
final long identityToken = Binder.clearCallingIdentity();
try {
- connection.findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId,
- partialInteractiveRegion, interactionId, callback, mFetchFlags | flags,
- interrogatingPid, interrogatingTid, spec, arguments);
- return true;
+ connection.getRemote().findAccessibilityNodeInfoByAccessibilityId(
+ accessibilityNodeId, partialInteractiveRegion, interactionId, callback,
+ mFetchFlags | flags, interrogatingPid, interrogatingTid, spec, arguments);
+ return mSecurityPolicy.computeValidReportedPackages(callingUid,
+ connection.getPackageName(), connection.getUid());
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
@@ -630,36 +583,36 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
} finally {
Binder.restoreCallingIdentity(identityToken);
// Recycle if passed to another process.
- if (partialInteractiveRegion != null && Binder.isProxy(connection)) {
+ if (partialInteractiveRegion != null && Binder.isProxy(connection.getRemote())) {
partialInteractiveRegion.recycle();
}
}
- return false;
+ return null;
}
@Override
- public boolean findFocus(int accessibilityWindowId, long accessibilityNodeId,
+ public String[] findFocus(int accessibilityWindowId, long accessibilityNodeId,
int focusType, int interactionId,
IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
throws RemoteException {
final int resolvedWindowId;
- IAccessibilityInteractionConnection connection = null;
+ RemoteAccessibilityConnection connection;
Region partialInteractiveRegion = Region.obtain();
MagnificationSpec spec;
synchronized (mLock) {
if (!isCalledForCurrentUserLocked()) {
- return false;
+ return null;
}
resolvedWindowId = resolveAccessibilityWindowIdForFindFocusLocked(
accessibilityWindowId, focusType);
final boolean permissionGranted =
mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
if (!permissionGranted) {
- return false;
+ return null;
} else {
connection = mSystemSupport.getConnectionLocked(resolvedWindowId);
if (connection == null) {
- return false;
+ return null;
}
}
if (!mSecurityPolicy.computePartialInteractiveRegionForWindowLocked(
@@ -672,12 +625,14 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
final int interrogatingPid = Binder.getCallingPid();
callback = mSystemSupport.replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
interrogatingPid, interrogatingTid);
+ final int callingUid = Binder.getCallingUid();
final long identityToken = Binder.clearCallingIdentity();
try {
- connection.findFocus(accessibilityNodeId, focusType, partialInteractiveRegion,
- interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid,
- spec);
- return true;
+ connection.getRemote().findFocus(accessibilityNodeId, focusType,
+ partialInteractiveRegion, interactionId, callback, mFetchFlags,
+ interrogatingPid, interrogatingTid, spec);
+ return mSecurityPolicy.computeValidReportedPackages(callingUid,
+ connection.getPackageName(), connection.getUid());
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling findFocus()");
@@ -685,35 +640,35 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
} finally {
Binder.restoreCallingIdentity(identityToken);
// Recycle if passed to another process.
- if (partialInteractiveRegion != null && Binder.isProxy(connection)) {
+ if (partialInteractiveRegion != null && Binder.isProxy(connection.getRemote())) {
partialInteractiveRegion.recycle();
}
}
- return false;
+ return null;
}
@Override
- public boolean focusSearch(int accessibilityWindowId, long accessibilityNodeId,
+ public String[] focusSearch(int accessibilityWindowId, long accessibilityNodeId,
int direction, int interactionId,
IAccessibilityInteractionConnectionCallback callback, long interrogatingTid)
throws RemoteException {
final int resolvedWindowId;
- IAccessibilityInteractionConnection connection = null;
+ RemoteAccessibilityConnection connection;
Region partialInteractiveRegion = Region.obtain();
MagnificationSpec spec;
synchronized (mLock) {
if (!isCalledForCurrentUserLocked()) {
- return false;
+ return null;
}
resolvedWindowId = resolveAccessibilityWindowIdLocked(accessibilityWindowId);
final boolean permissionGranted =
mSecurityPolicy.canGetAccessibilityNodeInfoLocked(this, resolvedWindowId);
if (!permissionGranted) {
- return false;
+ return null;
} else {
connection = mSystemSupport.getConnectionLocked(resolvedWindowId);
if (connection == null) {
- return false;
+ return null;
}
}
if (!mSecurityPolicy.computePartialInteractiveRegionForWindowLocked(
@@ -726,12 +681,14 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
final int interrogatingPid = Binder.getCallingPid();
callback = mSystemSupport.replaceCallbackIfNeeded(callback, resolvedWindowId, interactionId,
interrogatingPid, interrogatingTid);
+ final int callingUid = Binder.getCallingUid();
final long identityToken = Binder.clearCallingIdentity();
try {
- connection.focusSearch(accessibilityNodeId, direction, partialInteractiveRegion,
- interactionId, callback, mFetchFlags, interrogatingPid, interrogatingTid,
- spec);
- return true;
+ connection.getRemote().focusSearch(accessibilityNodeId, direction,
+ partialInteractiveRegion, interactionId, callback, mFetchFlags,
+ interrogatingPid, interrogatingTid, spec);
+ return mSecurityPolicy.computeValidReportedPackages(callingUid,
+ connection.getPackageName(), connection.getUid());
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling accessibilityFocusSearch()");
@@ -739,11 +696,11 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
} finally {
Binder.restoreCallingIdentity(identityToken);
// Recycle if passed to another process.
- if (partialInteractiveRegion != null && Binder.isProxy(connection)) {
+ if (partialInteractiveRegion != null && Binder.isProxy(connection.getRemote())) {
partialInteractiveRegion.recycle();
}
}
- return false;
+ return null;
}
@Override
@@ -784,6 +741,9 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
@Override
public boolean isFingerprintGestureDetectionAvailable() {
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
+ return false;
+ }
if (isCapturingFingerprintGestures()) {
FingerprintGestureDispatcher dispatcher =
mSystemSupport.getFingerprintGestureDispatcher();
@@ -1003,13 +963,15 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
public void notifyAccessibilityEvent(AccessibilityEvent event) {
synchronized (mLock) {
+ final int eventType = event.getEventType();
+
final boolean serviceWantsEvent = wantsEventLocked(event);
- if (!serviceWantsEvent && !mUsesAccessibilityCache &&
- ((AccessibilityCache.CACHE_CRITICAL_EVENTS_MASK & event.getEventType()) == 0)) {
+ final boolean requiredForCacheConsistency = mUsesAccessibilityCache
+ && ((AccessibilityCache.CACHE_CRITICAL_EVENTS_MASK & eventType) != 0);
+ if (!serviceWantsEvent && !requiredForCacheConsistency) {
return;
}
- final int eventType = event.getEventType();
// Make a copy since during dispatch it is possible the event to
// be modified to remove its source if the receiving service does
// not have permission to access the window content.
@@ -1275,6 +1237,10 @@ abstract class AccessibilityClientConnection extends IAccessibilityServiceConnec
return windowId;
}
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
private final class InvocationHandler extends Handler {
public static final int MSG_ON_GESTURE = 1;
public static final int MSG_CLEAR_ACCESSIBILITY_CACHE = 2;
diff --git a/com/android/server/accessibility/AccessibilityManagerService.java b/com/android/server/accessibility/AccessibilityManagerService.java
index 8b5c85a7..50b0be1a 100644
--- a/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/com/android/server/accessibility/AccessibilityManagerService.java
@@ -20,16 +20,19 @@ import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
+import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
+
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
-import android.accessibilityservice.IAccessibilityServiceConnection;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManagerInternal;
import android.app.AlertDialog;
+import android.app.AppOpsManager;
import android.app.PendingIntent;
+import android.appwidget.AppWidgetManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -61,7 +64,9 @@ import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.os.ServiceManager;
+import android.os.ShellCallback;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
@@ -72,6 +77,7 @@ import android.provider.SettingsStringUtil.ComponentNameSet;
import android.provider.SettingsStringUtil.SettingStringHelper;
import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
+import android.util.ArraySet;
import android.util.IntArray;
import android.util.Slog;
import android.util.SparseArray;
@@ -82,7 +88,6 @@ import android.view.MagnificationSpec;
import android.view.View;
import android.view.WindowInfo;
import android.view.WindowManager;
-import android.view.accessibility.AccessibilityCache;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityInteractionClient;
import android.view.accessibility.AccessibilityManager;
@@ -97,18 +102,22 @@ import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.IntPair;
import com.android.server.LocalServices;
-import com.android.server.policy.AccessibilityShortcutController;
+import com.android.internal.accessibility.AccessibilityShortcutController;
+import com.android.internal.accessibility.AccessibilityShortcutController.ToggleableFrameworkFeatureInfo;
import com.android.server.wm.WindowManagerInternal;
+import libcore.util.EmptyArray;
import org.xmlpull.v1.XmlPullParserException;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -126,7 +135,7 @@ import java.util.function.Consumer;
* on the device. Events are dispatched to {@link AccessibilityService}s.
*/
public class AccessibilityManagerService extends IAccessibilityManager.Stub
- implements AccessibilityClientConnection.SystemSupport {
+ implements AbstractAccessibilityServiceConnection.SystemSupport {
private static final boolean DEBUG = false;
@@ -185,8 +194,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private final WindowManagerInternal mWindowManagerService;
+ private AppWidgetManagerInternal mAppWidgetService;
+
private final SecurityPolicy mSecurityPolicy;
+ private final AppOpsManager mAppOpsManager;
+
private final MainHandler mMainHandler;
private final GlobalActionPerformer mGlobalActionPerformer;
@@ -217,10 +230,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private final RemoteCallbackList<IAccessibilityManagerClient> mGlobalClients =
new RemoteCallbackList<>();
- private final SparseArray<AccessibilityConnectionWrapper> mGlobalInteractionConnections =
+ private final SparseArray<RemoteAccessibilityConnection> mGlobalInteractionConnections =
new SparseArray<>();
- private AccessibilityConnectionWrapper mPictureInPictureActionReplacingConnection;
+ private RemoteAccessibilityConnection mPictureInPictureActionReplacingConnection;
private final SparseArray<IBinder> mGlobalWindowTokens = new SparseArray<>();
@@ -255,6 +268,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mWindowManagerService = LocalServices.getService(WindowManagerInternal.class);
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
mSecurityPolicy = new SecurityPolicy();
+ mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mMainHandler = new MainHandler(mContext.getMainLooper());
mGlobalActionPerformer = new GlobalActionPerformer(mContext, mWindowManagerService);
@@ -287,6 +301,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
return state;
}
+ boolean getBindInstantServiceAllowed(int userId) {
+ return mSecurityPolicy.getBindInstantServiceAllowed(userId);
+ }
+
+ void setBindInstantServiceAllowed(int userId, boolean allowed) {
+ mSecurityPolicy.setBindInstantServiceAllowed(userId, allowed);
+ }
+
private void registerBroadcastReceivers() {
PackageMonitor monitor = new PackageMonitor() {
@Override
@@ -445,26 +467,29 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
@Override
- public long addClient(IAccessibilityManagerClient client, int userId) {
+ public long addClient(IAccessibilityManagerClient callback, int userId) {
synchronized (mLock) {
// We treat calls from a profile as if made by its parent as profiles
// share the accessibility state of the parent. The call below
// performs the current profile parent resolution.
final int resolvedUserId = mSecurityPolicy
.resolveCallingUserIdEnforcingPermissionsLocked(userId);
+
// If the client is from a process that runs across users such as
// the system UI or the system we add it to the global state that
// is shared across users.
UserState userState = getUserStateLocked(resolvedUserId);
+ Client client = new Client(callback, Binder.getCallingUid(), userState);
if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) {
- mGlobalClients.register(client);
+ mGlobalClients.register(callback, client);
if (DEBUG) {
Slog.i(LOG_TAG, "Added global client for pid:" + Binder.getCallingPid());
}
return IntPair.of(
- userState.getClientState(), userState.mLastSentRelevantEventTypes);
+ userState.getClientState(),
+ client.mLastSentRelevantEventTypes);
} else {
- userState.mUserClients.register(client);
+ userState.mUserClients.register(callback, client);
// If this client is not for the current user we do not
// return a state since it is not for the foreground user.
// We will send the state to the client on a user switch.
@@ -474,7 +499,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
return IntPair.of(
(resolvedUserId == mCurrentUserId) ? userState.getClientState() : 0,
- userState.mLastSentRelevantEventTypes);
+ client.mLastSentRelevantEventTypes);
}
}
}
@@ -496,9 +521,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
// We treat calls from a profile as if made by its parent as profiles
// share the accessibility state of the parent. The call below
- // performs the current profile parent resolution..
+ // performs the current profile parent resolution.
final int resolvedUserId = mSecurityPolicy
.resolveCallingUserIdEnforcingPermissionsLocked(userId);
+
+ // Make sure the reported package is one the caller has access to.
+ event.setPackageName(mSecurityPolicy.resolveValidReportedPackageLocked(
+ event.getPackageName(), UserHandle.getCallingAppId(), resolvedUserId));
+
// This method does nothing for a background user.
if (resolvedUserId == mCurrentUserId) {
if (mSecurityPolicy.canDispatchAccessibilityEventLocked(event)) {
@@ -616,30 +646,38 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
@Override
public int addAccessibilityInteractionConnection(IWindow windowToken,
- IAccessibilityInteractionConnection connection, int userId) throws RemoteException {
+ IAccessibilityInteractionConnection connection, String packageName,
+ int userId) throws RemoteException {
synchronized (mLock) {
// We treat calls from a profile as if made by its parent as profiles
// share the accessibility state of the parent. The call below
// performs the current profile parent resolution.
final int resolvedUserId = mSecurityPolicy
.resolveCallingUserIdEnforcingPermissionsLocked(userId);
+ final int resolvedUid = UserHandle.getUid(resolvedUserId, UserHandle.getCallingAppId());
+
+ // Make sure the reported package is one the caller has access to.
+ packageName = mSecurityPolicy.resolveValidReportedPackageLocked(
+ packageName, UserHandle.getCallingAppId(), resolvedUserId);
+
final int windowId = sNextWindowId++;
// If the window is from a process that runs across users such as
// the system UI or the system we add it to the global state that
// is shared across users.
if (mSecurityPolicy.isCallerInteractingAcrossUsers(userId)) {
- AccessibilityConnectionWrapper wrapper = new AccessibilityConnectionWrapper(
- windowId, connection, UserHandle.USER_ALL);
+ RemoteAccessibilityConnection wrapper = new RemoteAccessibilityConnection(
+ windowId, connection, packageName, resolvedUid, UserHandle.USER_ALL);
wrapper.linkToDeath();
mGlobalInteractionConnections.put(windowId, wrapper);
mGlobalWindowTokens.put(windowId, windowToken.asBinder());
if (DEBUG) {
Slog.i(LOG_TAG, "Added global connection for pid:" + Binder.getCallingPid()
- + " with windowId: " + windowId + " and token: " + windowToken.asBinder());
+ + " with windowId: " + windowId + " and token: "
+ + windowToken.asBinder());
}
} else {
- AccessibilityConnectionWrapper wrapper = new AccessibilityConnectionWrapper(
- windowId, connection, resolvedUserId);
+ RemoteAccessibilityConnection wrapper = new RemoteAccessibilityConnection(
+ windowId, connection, packageName, resolvedUid, resolvedUserId);
wrapper.linkToDeath();
UserState userState = getUserStateLocked(resolvedUserId);
userState.mInteractionConnections.put(windowId, wrapper);
@@ -692,13 +730,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private int removeAccessibilityInteractionConnectionInternalLocked(IBinder windowToken,
SparseArray<IBinder> windowTokens,
- SparseArray<AccessibilityConnectionWrapper> interactionConnections) {
+ SparseArray<RemoteAccessibilityConnection> interactionConnections) {
final int count = windowTokens.size();
for (int i = 0; i < count; i++) {
if (windowTokens.valueAt(i) == windowToken) {
final int windowId = windowTokens.keyAt(i);
windowTokens.removeAt(i);
- AccessibilityConnectionWrapper wrapper = interactionConnections.get(windowId);
+ RemoteAccessibilityConnection wrapper = interactionConnections.get(windowId);
wrapper.unlinkToDeath();
interactionConnections.remove(windowId);
return windowId;
@@ -718,9 +756,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mPictureInPictureActionReplacingConnection = null;
}
if (connection != null) {
- AccessibilityConnectionWrapper wrapper = new AccessibilityConnectionWrapper(
+ RemoteAccessibilityConnection wrapper = new RemoteAccessibilityConnection(
AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID,
- connection, UserHandle.USER_ALL);
+ connection, "foo.bar.baz", Process.SYSTEM_UID, UserHandle.USER_ALL);
mPictureInPictureActionReplacingConnection = wrapper;
wrapper.linkToDeath();
}
@@ -748,7 +786,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
public void unregisterUiTestAutomationService(IAccessibilityServiceClient serviceClient) {
synchronized (mLock) {
mUiAutomationManager.unregisterUiTestAutomationServiceLocked(serviceClient);
- onUserStateChangedLocked(getCurrentUserStateLocked());
}
}
@@ -928,7 +965,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
* Has no effect if no item has accessibility focus, if the item with accessibility
* focus does not expose the specified action, or if the action fails.
*
- * @param actionId The id of the action to perform.
+ * @param action The action to perform.
*
* @return {@code true} if the action was performed. {@code false} if it was not.
*/
@@ -1191,26 +1228,27 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private boolean readInstalledAccessibilityServiceLocked(UserState userState) {
mTempAccessibilityServiceInfoList.clear();
+ int flags = PackageManager.GET_SERVICES
+ | PackageManager.GET_META_DATA
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+
+ if (userState.mBindInstantServiceAllowed) {
+ flags |= PackageManager.MATCH_INSTANT;
+ }
+
List<ResolveInfo> installedServices = mPackageManager.queryIntentServicesAsUser(
- new Intent(AccessibilityService.SERVICE_INTERFACE),
- PackageManager.GET_SERVICES
- | PackageManager.GET_META_DATA
- | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
- | PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
- mCurrentUserId);
+ new Intent(AccessibilityService.SERVICE_INTERFACE), flags, mCurrentUserId);
for (int i = 0, count = installedServices.size(); i < count; i++) {
ResolveInfo resolveInfo = installedServices.get(i);
ServiceInfo serviceInfo = resolveInfo.serviceInfo;
- if (!android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE.equals(
- serviceInfo.permission)) {
- Slog.w(LOG_TAG, "Skipping accessibilty service " + new ComponentName(
- serviceInfo.packageName, serviceInfo.name).flattenToShortString()
- + ": it does not require the permission "
- + android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE);
+
+ if (!canRegisterService(serviceInfo)) {
continue;
}
+
AccessibilityServiceInfo accessibilityServiceInfo;
try {
accessibilityServiceInfo = new AccessibilityServiceInfo(resolveInfo, mContext);
@@ -1231,6 +1269,28 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
return false;
}
+ private boolean canRegisterService(ServiceInfo serviceInfo) {
+ if (!android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE.equals(
+ serviceInfo.permission)) {
+ Slog.w(LOG_TAG, "Skipping accessibility service " + new ComponentName(
+ serviceInfo.packageName, serviceInfo.name).flattenToShortString()
+ + ": it does not require the permission "
+ + android.Manifest.permission.BIND_ACCESSIBILITY_SERVICE);
+ return false;
+ }
+
+ int servicePackageUid = serviceInfo.applicationInfo.uid;
+ if (mAppOpsManager.noteOpNoThrow(AppOpsManager.OP_BIND_ACCESSIBILITY_SERVICE,
+ servicePackageUid, serviceInfo.packageName) != AppOpsManager.MODE_ALLOWED) {
+ Slog.w(LOG_TAG, "Skipping accessibility service " + new ComponentName(
+ serviceInfo.packageName, serviceInfo.name).flattenToShortString()
+ + ": disallowed by AppOps");
+ return false;
+ }
+
+ return true;
+ }
+
private boolean readEnabledAccessibilityServicesLocked(UserState userState) {
mTempComponentNameSet.clear();
readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
@@ -1287,33 +1347,67 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
private void updateRelevantEventsLocked(UserState userState) {
- int relevantEventTypes = AccessibilityCache.CACHE_CRITICAL_EVENTS_MASK;
- for (AccessibilityServiceConnection service : userState.mBoundServices) {
- relevantEventTypes |= service.mEventTypes;
- }
- relevantEventTypes |= mUiAutomationManager.getRequestedEventMaskLocked();
- int finalRelevantEventTypes = relevantEventTypes;
-
- if (userState.mLastSentRelevantEventTypes != finalRelevantEventTypes) {
- userState.mLastSentRelevantEventTypes = finalRelevantEventTypes;
- mMainHandler.obtainMessage(MainHandler.MSG_SEND_RELEVANT_EVENTS_CHANGED_TO_CLIENTS,
- userState.mUserId, finalRelevantEventTypes);
- mMainHandler.post(() -> {
- broadcastToClients(userState, (client) -> {
- try {
- client.setRelevantEventTypes(finalRelevantEventTypes);
- } catch (RemoteException re) {
- /* ignore */
- }
- });
- });
+ mMainHandler.post(() -> {
+ broadcastToClients(userState, ignoreRemoteException(client -> {
+ int relevantEventTypes = computeRelevantEventTypes(userState, client);
+
+ if (client.mLastSentRelevantEventTypes != relevantEventTypes) {
+ client.mLastSentRelevantEventTypes = relevantEventTypes;
+ client.mCallback.setRelevantEventTypes(relevantEventTypes);
+ }
+ }));
+ });
+ }
+
+ private int computeRelevantEventTypes(UserState userState, Client client) {
+ int relevantEventTypes = 0;
+
+ int numBoundServices = userState.mBoundServices.size();
+ for (int i = 0; i < numBoundServices; i++) {
+ AccessibilityServiceConnection service =
+ userState.mBoundServices.get(i);
+ relevantEventTypes |= isClientInPackageWhitelist(service.getServiceInfo(), client)
+ ? service.getRelevantEventTypes()
+ : 0;
+ }
+ relevantEventTypes |= isClientInPackageWhitelist(
+ mUiAutomationManager.getServiceInfo(), client)
+ ? mUiAutomationManager.getRelevantEventTypes()
+ : 0;
+ return relevantEventTypes;
+ }
+
+ private static boolean isClientInPackageWhitelist(
+ @Nullable AccessibilityServiceInfo serviceInfo, Client client) {
+ if (serviceInfo == null) return false;
+
+ String[] clientPackages = client.mPackageNames;
+ boolean result = ArrayUtils.isEmpty(serviceInfo.packageNames);
+ if (!result && clientPackages != null) {
+ for (String packageName : clientPackages) {
+ if (ArrayUtils.contains(serviceInfo.packageNames, packageName)) {
+ result = true;
+ break;
+ }
+ }
+ }
+ if (!result) {
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "Dropping events: "
+ + Arrays.toString(clientPackages) + " -> "
+ + serviceInfo.getComponentName().flattenToShortString()
+ + " due to not being in package whitelist "
+ + Arrays.toString(serviceInfo.packageNames));
+ }
}
+
+ return result;
}
private void broadcastToClients(
- UserState userState, Consumer<IAccessibilityManagerClient> clientAction) {
- mGlobalClients.broadcast(clientAction);
- userState.mUserClients.broadcast(clientAction);
+ UserState userState, Consumer<Client> clientAction) {
+ mGlobalClients.broadcastForEachCookie(clientAction);
+ userState.mUserClients.broadcastForEachCookie(clientAction);
}
private void unbindAllServicesLocked(UserState userState) {
@@ -1897,8 +1991,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
if (userState.mServiceToEnableWithShortcut == null) {
return;
}
- boolean shortcutServiceIsInstalled = false;
- for (int i = 0; i < userState.mInstalledServices.size(); i++) {
+ boolean shortcutServiceIsInstalled =
+ AccessibilityShortcutController.getFrameworkShortcutFeaturesMap()
+ .containsKey(userState.mServiceToEnableWithShortcut);
+ for (int i = 0; !shortcutServiceIsInstalled && (i < userState.mInstalledServices.size());
+ i++) {
if (userState.mInstalledServices.get(i).getComponentName()
.equals(userState.mServiceToEnableWithShortcut)) {
shortcutServiceIsInstalled = true;
@@ -1909,7 +2006,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
final long identity = Binder.clearCallingIdentity();
try {
Settings.Secure.putStringForUser(mContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, null, userState.mUserId);
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, null,
+ userState.mUserId);
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 0, userState.mUserId);
@@ -2110,6 +2208,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
* permission to write secure settings, since someone with that permission can enable
* accessibility services themselves.
*/
+ @Override
public void performAccessibilityShortcut() {
if ((UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)
&& (mContext.checkCallingPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
@@ -2117,12 +2216,26 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
throw new SecurityException(
"performAccessibilityShortcut requires the WRITE_SECURE_SETTINGS permission");
}
+ final Map<ComponentName, ToggleableFrameworkFeatureInfo> frameworkFeatureMap =
+ AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
synchronized(mLock) {
- UserState userState = getUserStateLocked(mCurrentUserId);
- ComponentName serviceName = userState.mServiceToEnableWithShortcut;
+ final UserState userState = getUserStateLocked(mCurrentUserId);
+ final ComponentName serviceName = userState.mServiceToEnableWithShortcut;
if (serviceName == null) {
return;
}
+ if (frameworkFeatureMap.containsKey(serviceName)) {
+ // Toggle the requested framework feature
+ ToggleableFrameworkFeatureInfo featureInfo = frameworkFeatureMap.get(serviceName);
+ SettingStringHelper setting = new SettingStringHelper(mContext.getContentResolver(),
+ featureInfo.getSettingKey(), mCurrentUserId);
+ // Assuming that the default state will be to have the feature off
+ if (!TextUtils.equals(featureInfo.getSettingOnValue(), setting.read())) {
+ setting.write(featureInfo.getSettingOnValue());
+ } else {
+ setting.write(featureInfo.getSettingOffValue());
+ }
+ }
final long identity = Binder.clearCallingIdentity();
try {
if (userState.mComponentNameToServiceMap.get(serviceName) == null) {
@@ -2245,18 +2358,35 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
}
- private class AccessibilityConnectionWrapper implements DeathRecipient {
+ class RemoteAccessibilityConnection implements DeathRecipient {
+ private final int mUid;
+ private final String mPackageName;
private final int mWindowId;
private final int mUserId;
private final IAccessibilityInteractionConnection mConnection;
- public AccessibilityConnectionWrapper(int windowId,
- IAccessibilityInteractionConnection connection, int userId) {
+ RemoteAccessibilityConnection(int windowId,
+ IAccessibilityInteractionConnection connection,
+ String packageName, int uid, int userId) {
mWindowId = windowId;
+ mPackageName = packageName;
+ mUid = uid;
mUserId = userId;
mConnection = connection;
}
+ public int getUid() {
+ return mUid;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public IAccessibilityInteractionConnection getRemote() {
+ return mConnection;
+ }
+
public void linkToDeath() throws RemoteException {
mConnection.asBinder().linkToDeath(this, 0);
}
@@ -2368,13 +2498,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
synchronized (mLock) {
userState = getUserStateLocked(userId);
}
- broadcastToClients(userState, (client) -> {
- try {
- client.setRelevantEventTypes(relevantEventTypes);
- } catch (RemoteException re) {
- /* ignore */
- }
- });
+ broadcastToClients(userState, ignoreRemoteException(
+ client -> client.mCallback.setRelevantEventTypes(relevantEventTypes)));
} break;
case MSG_SEND_ACCESSIBILITY_BUTTON_TO_INPUT_FILTER: {
@@ -2400,8 +2525,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private void announceNewUserIfNeeded() {
synchronized (mLock) {
UserState userState = getCurrentUserStateLocked();
- if (userState.isHandlingAccessibilityEvents()
- && userState.isObservedEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT)) {
+ if (userState.isHandlingAccessibilityEvents()) {
UserManager userManager = (UserManager) mContext.getSystemService(
Context.USER_SERVICE);
String message = mContext.getString(R.string.user_switched,
@@ -2424,30 +2548,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private void sendStateToClients(int clientState,
RemoteCallbackList<IAccessibilityManagerClient> clients) {
- clients.broadcast((client) -> {
- try {
- client.setState(clientState);
- } catch (RemoteException re) {
- /* ignore */
- }
- });
+ clients.broadcast(ignoreRemoteException(
+ client -> client.setState(clientState)));
}
private void notifyClientsOfServicesStateChange(
RemoteCallbackList<IAccessibilityManagerClient> clients) {
- try {
- final int userClientCount = clients.beginBroadcast();
- for (int i = 0; i < userClientCount; i++) {
- IAccessibilityManagerClient client = clients.getBroadcastItem(i);
- try {
- client.notifyServicesStateChanged();
- } catch (RemoteException re) {
- /* ignore */
- }
- }
- } finally {
- clients.finishBroadcast();
- }
+ clients.broadcast(ignoreRemoteException(
+ client -> client.notifyServicesStateChanged()));
}
}
@@ -2514,11 +2622,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
long accessibilityNodeId, int action, Bundle arguments, int interactionId,
IAccessibilityInteractionConnectionCallback callback, int fetchFlags,
long interrogatingTid) {
- IAccessibilityInteractionConnection connection = null;
+ RemoteAccessibilityConnection connection;
IBinder activityToken = null;
synchronized (mLock) {
connection = getConnectionLocked(resolvedWindowId);
- if (connection == null) return false;
+ if (connection == null) {
+ return false;
+ }
final boolean isA11yFocusAction = (action == ACTION_ACCESSIBILITY_FOCUS)
|| (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS);
final AccessibilityWindowInfo a11yWindowInfo =
@@ -2530,7 +2640,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
if ((a11yWindowInfo != null) && a11yWindowInfo.isInPictureInPictureMode()
&& (mPictureInPictureActionReplacingConnection != null) && !isA11yFocusAction) {
- connection = mPictureInPictureActionReplacingConnection.mConnection;
+ connection = mPictureInPictureActionReplacingConnection;
}
}
final int interrogatingPid = Binder.getCallingPid();
@@ -2545,8 +2655,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
LocalServices.getService(ActivityManagerInternal.class)
.setFocusedActivity(activityToken);
}
- connection.performAccessibilityAction(accessibilityNodeId, action, arguments,
- interactionId, callback, fetchFlags, interrogatingPid, interrogatingTid);
+ connection.mConnection.performAccessibilityAction(accessibilityNodeId, action,
+ arguments, interactionId, callback, fetchFlags, interrogatingPid,
+ interrogatingTid);
} catch (RemoteException re) {
if (DEBUG) {
Slog.e(LOG_TAG, "Error calling performAccessibilityAction: " + re);
@@ -2559,17 +2670,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
@Override
- public IAccessibilityInteractionConnection getConnectionLocked(int windowId) {
+ public RemoteAccessibilityConnection getConnectionLocked(int windowId) {
if (DEBUG) {
Slog.i(LOG_TAG, "Trying to get interaction connection to windowId: " + windowId);
}
- AccessibilityManagerService.AccessibilityConnectionWrapper wrapper =
+ RemoteAccessibilityConnection connection =
mGlobalInteractionConnections.get(windowId);
- if (wrapper == null) {
- wrapper = getCurrentUserStateLocked().mInteractionConnections.get(windowId);
+ if (connection == null) {
+ connection = getCurrentUserStateLocked().mInteractionConnections.get(windowId);
}
- if (wrapper != null && wrapper.mConnection != null) {
- return wrapper.mConnection;
+ if (connection != null && connection.mConnection != null) {
+ return connection;
}
if (DEBUG) {
Slog.e(LOG_TAG, "No interaction connection to window: " + windowId);
@@ -2602,6 +2713,24 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
}
+ private AppWidgetManagerInternal getAppWidgetManager() {
+ synchronized (mLock) {
+ if (mAppWidgetService == null
+ && mPackageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS)) {
+ mAppWidgetService = LocalServices.getService(AppWidgetManagerInternal.class);
+ }
+ return mAppWidgetService;
+ }
+ }
+
+ @Override
+ public void onShellCommand(FileDescriptor in, FileDescriptor out,
+ FileDescriptor err, String[] args, ShellCallback callback,
+ ResultReceiver resultReceiver) {
+ new AccessibilityShellCommand(this).exec(this, in, out, err, args,
+ callback, resultReceiver);
+ }
+
final class WindowsForAccessibilityCallback implements
WindowManagerInternal.WindowsForAccessibilityCallback {
@@ -2894,6 +3023,107 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
}
+ private boolean isValidPackageForUid(String packageName, int uid) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return uid == mPackageManager.getPackageUidAsUser(
+ packageName, UserHandle.getUserId(uid));
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ String resolveValidReportedPackageLocked(CharSequence packageName, int appId, int userId) {
+ // Okay to pass no package
+ if (packageName == null) {
+ return null;
+ }
+ // The system gets to pass any package
+ if (appId == Process.SYSTEM_UID) {
+ return packageName.toString();
+ }
+ // Passing a package in your UID is fine
+ final String packageNameStr = packageName.toString();
+ final int resolvedUid = UserHandle.getUid(userId, appId);
+ if (isValidPackageForUid(packageNameStr, resolvedUid)) {
+ return packageName.toString();
+ }
+ // Appwidget hosts get to pass packages for widgets they host
+ final AppWidgetManagerInternal appWidgetManager = getAppWidgetManager();
+ if (appWidgetManager != null && ArrayUtils.contains(appWidgetManager
+ .getHostedWidgetPackages(resolvedUid), packageNameStr)) {
+ return packageName.toString();
+ }
+ // Otherwise, set the package to the first one in the UID
+ final String[] packageNames = mPackageManager.getPackagesForUid(resolvedUid);
+ if (ArrayUtils.isEmpty(packageNames)) {
+ return null;
+ }
+ // Okay, the caller reported a package it does not have access to.
+ // Instead of crashing the caller for better backwards compatibility
+ // we report the first package in the UID. Since most of the time apps
+ // don't use shared user id, this will yield correct results and for
+ // the edge case of using a shared user id we may report the wrong
+ // package but this is fine since first, this is a cheating app and
+ // second there is no way to get the correct package anyway.
+ return packageNames[0];
+ }
+
+ String[] computeValidReportedPackages(int callingUid,
+ String targetPackage, int targetUid) {
+ if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {
+ // Empty array means any package is Okay
+ return EmptyArray.STRING;
+ }
+ // IMPORTANT: The target package is already vetted to be in the target UID
+ String[] uidPackages = new String[]{targetPackage};
+ // Appwidget hosts get to pass packages for widgets they host
+ final AppWidgetManagerInternal appWidgetManager = getAppWidgetManager();
+ if (appWidgetManager != null) {
+ final ArraySet<String> widgetPackages = appWidgetManager
+ .getHostedWidgetPackages(targetUid);
+ if (widgetPackages != null && !widgetPackages.isEmpty()) {
+ final String[] validPackages = new String[uidPackages.length
+ + widgetPackages.size()];
+ System.arraycopy(uidPackages, 0, validPackages, 0, uidPackages.length);
+ final int widgetPackageCount = widgetPackages.size();
+ for (int i = 0; i < widgetPackageCount; i++) {
+ validPackages[uidPackages.length + i] = widgetPackages.valueAt(i);
+ }
+ return validPackages;
+ }
+ }
+ return uidPackages;
+ }
+
+ private boolean getBindInstantServiceAllowed(int userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_BIND_INSTANT_SERVICE,
+ "getBindInstantServiceAllowed");
+ UserState state = mUserStates.get(userId);
+ return (state != null) && state.mBindInstantServiceAllowed;
+ }
+
+ private void setBindInstantServiceAllowed(int userId, boolean allowed) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_BIND_INSTANT_SERVICE,
+ "setBindInstantServiceAllowed");
+ UserState state = mUserStates.get(userId);
+ if (state == null) {
+ if (!allowed) {
+ return;
+ }
+ state = new UserState(userId);
+ mUserStates.put(userId, state);
+ }
+ if (state.mBindInstantServiceAllowed != allowed) {
+ state.mBindInstantServiceAllowed = allowed;
+ onUserStateChangedLocked(state);
+ }
+ }
+
public void clearWindowsLocked() {
List<WindowInfo> windows = Collections.emptyList();
final int activeWindowId = mActiveWindowId;
@@ -3158,41 +3388,33 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
if (mWindowsForAccessibilityCallback == null) {
return;
}
- final int userId;
- synchronized (mLock) {
- userId = mCurrentUserId;
- final UserState userState = getUserStateLocked(userId);
- if (!userState.isObservedEventType(AccessibilityEvent.TYPE_WINDOWS_CHANGED)) {
- return;
- }
- }
final long identity = Binder.clearCallingIdentity();
try {
// Let the client know the windows changed.
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_WINDOWS_CHANGED);
event.setEventTime(SystemClock.uptimeMillis());
- sendAccessibilityEvent(event, userId);
+ sendAccessibilityEvent(event, mCurrentUserId);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
public boolean canGetAccessibilityNodeInfoLocked(
- AccessibilityClientConnection service, int windowId) {
+ AbstractAccessibilityServiceConnection service, int windowId) {
return canRetrieveWindowContentLocked(service) && isRetrievalAllowingWindow(windowId);
}
- public boolean canRetrieveWindowsLocked(AccessibilityClientConnection service) {
+ public boolean canRetrieveWindowsLocked(AbstractAccessibilityServiceConnection service) {
return canRetrieveWindowContentLocked(service) && service.mRetrieveInteractiveWindows;
}
- public boolean canRetrieveWindowContentLocked(AccessibilityClientConnection service) {
+ public boolean canRetrieveWindowContentLocked(AbstractAccessibilityServiceConnection service) {
return (service.mAccessibilityServiceInfo.getCapabilities()
& AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT) != 0;
}
- public boolean canControlMagnification(AccessibilityClientConnection service) {
+ public boolean canControlMagnification(AbstractAccessibilityServiceConnection service) {
return (service.mAccessibilityServiceInfo.getCapabilities()
& AccessibilityServiceInfo.CAPABILITY_CAN_CONTROL_MAGNIFICATION) != 0;
}
@@ -3289,7 +3511,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
final int windowCount = mWindows.size();
for (int i = 0; i < windowCount; i++) {
AccessibilityWindowInfo window = mWindows.get(i);
- if (window.inPictureInPicture()) {
+ if (window.isInPictureInPictureMode()) {
return window;
}
}
@@ -3320,15 +3542,28 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
}
+ /** Represents an {@link AccessibilityManager} */
+ class Client {
+ final IAccessibilityManagerClient mCallback;
+ final String[] mPackageNames;
+ int mLastSentRelevantEventTypes;
+
+ private Client(IAccessibilityManagerClient callback, int clientUid, UserState userState) {
+ mCallback = callback;
+ mPackageNames = mPackageManager.getPackagesForUid(clientUid);
+ mLastSentRelevantEventTypes = computeRelevantEventTypes(userState, this);
+ }
+ }
+
public class UserState {
public final int mUserId;
// Non-transient state.
public final RemoteCallbackList<IAccessibilityManagerClient> mUserClients =
- new RemoteCallbackList<>();
+ new RemoteCallbackList<>();
- public final SparseArray<AccessibilityConnectionWrapper> mInteractionConnections =
+ public final SparseArray<RemoteAccessibilityConnection> mInteractionConnections =
new SparseArray<>();
public final SparseArray<IBinder> mWindowTokens = new SparseArray<>();
@@ -3338,8 +3573,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
public final CopyOnWriteArrayList<AccessibilityServiceConnection> mBoundServices =
new CopyOnWriteArrayList<>();
- public int mLastSentRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK;
-
public final Map<ComponentName, AccessibilityServiceConnection> mComponentNameToServiceMap =
new HashMap<>();
@@ -3373,14 +3606,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
public boolean mIsFilterKeyEventsEnabled;
public boolean mAccessibilityFocusOnlyInActiveWindow;
+ public boolean mBindInstantServiceAllowed;
+
public UserState(int userId) {
mUserId = userId;
}
- public boolean isObservedEventType(@AccessibilityEvent.EventType int type) {
- return (mLastSentRelevantEventTypes & type) != 0;
- }
-
public int getClientState() {
int clientState = 0;
final boolean a11yEnabled = (mUiAutomationManager.isUiAutomationRunningLocked()
diff --git a/com/android/server/accessibility/AccessibilityServiceConnection.java b/com/android/server/accessibility/AccessibilityServiceConnection.java
index 9cafa1e7..96b89792 100644
--- a/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -19,9 +19,7 @@ package com.android.server.accessibility;
import static android.provider.Settings.Secure.SHOW_MODE_AUTO;
import android.accessibilityservice.AccessibilityServiceInfo;
-import android.accessibilityservice.GestureDescription;
import android.accessibilityservice.IAccessibilityServiceClient;
-import android.accessibilityservice.IAccessibilityServiceConnection;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -39,7 +37,6 @@ import com.android.server.accessibility.AccessibilityManagerService.UserState;
import com.android.server.wm.WindowManagerInternal;
import java.lang.ref.WeakReference;
-import java.util.List;
import java.util.Set;
/**
@@ -50,7 +47,7 @@ import java.util.Set;
* passed to the service it represents as soon it is bound. It also serves as the
* connection for the service.
*/
-class AccessibilityServiceConnection extends AccessibilityClientConnection {
+class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnection {
private static final String LOG_TAG = "AccessibilityServiceConnection";
/*
Holding a weak reference so there isn't a loop of references. UserState keeps lists of bound
@@ -94,10 +91,12 @@ class AccessibilityServiceConnection extends AccessibilityClientConnection {
if (userState == null) return;
final long identity = Binder.clearCallingIdentity();
try {
+ int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE;
+ if (userState.mBindInstantServiceAllowed) {
+ flags |= Context.BIND_ALLOW_INSTANT;
+ }
if (mService == null && mContext.bindServiceAsUser(
- mIntent, this,
- Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
- new UserHandle(userState.mUserId))) {
+ mIntent, this, flags, new UserHandle(userState.mUserId))) {
userState.getBindingServicesLocked().add(mComponentName);
}
} finally {
diff --git a/com/android/server/accessibility/AccessibilityShellCommand.java b/com/android/server/accessibility/AccessibilityShellCommand.java
new file mode 100644
index 00000000..ff59c24a
--- /dev/null
+++ b/com/android/server/accessibility/AccessibilityShellCommand.java
@@ -0,0 +1,100 @@
+/*
+ * 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 com.android.server.accessibility;
+
+import android.annotation.NonNull;
+import android.os.ShellCommand;
+import android.os.UserHandle;
+
+import java.io.PrintWriter;
+
+/**
+ * Shell command implementation for the accessibility manager service
+ */
+final class AccessibilityShellCommand extends ShellCommand {
+ final @NonNull AccessibilityManagerService mService;
+
+ AccessibilityShellCommand(@NonNull AccessibilityManagerService service) {
+ mService = service;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+ switch (cmd) {
+ case "get-bind-instant-service-allowed": {
+ return runGetBindInstantServiceAllowed();
+ }
+ case "set-bind-instant-service-allowed": {
+ return runSetBindInstantServiceAllowed();
+ }
+ }
+ return -1;
+ }
+
+ private int runGetBindInstantServiceAllowed() {
+ final Integer userId = parseUserId();
+ if (userId == null) {
+ return -1;
+ }
+ getOutPrintWriter().println(Boolean.toString(
+ mService.getBindInstantServiceAllowed(userId)));
+ return 0;
+ }
+
+ private int runSetBindInstantServiceAllowed() {
+ final Integer userId = parseUserId();
+ if (userId == null) {
+ return -1;
+ }
+ final String allowed = getNextArgRequired();
+ if (allowed == null) {
+ getErrPrintWriter().println("Error: no true/false specified");
+ return -1;
+ }
+ mService.setBindInstantServiceAllowed(userId,
+ Boolean.parseBoolean(allowed));
+ return 0;
+ }
+
+ private Integer parseUserId() {
+ final String option = getNextOption();
+ if (option != null) {
+ if (option.equals("--user")) {
+ return UserHandle.parseUserArg(getNextArgRequired());
+ } else {
+ getErrPrintWriter().println("Unknown option: " + option);
+ return null;
+ }
+ }
+ return UserHandle.USER_SYSTEM;
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("Accessibility service (accessibility) commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" set-bind-instant-service-allowed [--user <USER_ID>] true|false ");
+ pw.println(" Set whether binding to services provided by instant apps is allowed.");
+ pw.println(" get-bind-instant-service-allowed [--user <USER_ID>]");
+ pw.println(" Get whether binding to services provided by instant apps is allowed.");
+ }
+} \ No newline at end of file
diff --git a/com/android/server/accessibility/TouchExplorer.java b/com/android/server/accessibility/TouchExplorer.java
index 62017e87..3419b809 100644
--- a/com/android/server/accessibility/TouchExplorer.java
+++ b/com/android/server/accessibility/TouchExplorer.java
@@ -791,7 +791,7 @@ class TouchExplorer extends BaseEventStreamTransformation
*/
private void sendAccessibilityEvent(int type) {
AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mContext);
- if (accessibilityManager.isObservedEventType(type)) {
+ if (accessibilityManager.isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(type);
event.setWindowId(mAms.getActiveWindowId());
accessibilityManager.sendAccessibilityEvent(event);
diff --git a/com/android/server/accessibility/UiAutomationManager.java b/com/android/server/accessibility/UiAutomationManager.java
index f0571126..ed3b3e77 100644
--- a/com/android/server/accessibility/UiAutomationManager.java
+++ b/com/android/server/accessibility/UiAutomationManager.java
@@ -18,6 +18,7 @@ package com.android.server.accessibility;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
+import android.annotation.Nullable;
import android.app.UiAutomation;
import android.content.ComponentName;
import android.content.Context;
@@ -28,6 +29,7 @@ import android.os.RemoteException;
import android.util.Slog;
import android.view.accessibility.AccessibilityEvent;
+import com.android.internal.util.DumpUtils;
import com.android.server.wm.WindowManagerInternal;
import java.io.FileDescriptor;
@@ -45,6 +47,8 @@ class UiAutomationManager {
private AccessibilityServiceInfo mUiAutomationServiceInfo;
+ private AbstractAccessibilityServiceConnection.SystemSupport mSystemSupport;
+
private int mUiAutomationFlags;
private IBinder mUiAutomationServiceOwner;
@@ -75,7 +79,7 @@ class UiAutomationManager {
Context context, AccessibilityServiceInfo accessibilityServiceInfo,
int id, Handler mainHandler, Object lock,
AccessibilityManagerService.SecurityPolicy securityPolicy,
- AccessibilityClientConnection.SystemSupport systemSupport,
+ AbstractAccessibilityServiceConnection.SystemSupport systemSupport,
WindowManagerInternal windowManagerInternal,
GlobalActionPerformer globalActionPerfomer, int flags) {
accessibilityServiceInfo.setComponentName(COMPONENT_NAME);
@@ -92,6 +96,7 @@ class UiAutomationManager {
return;
}
+ mSystemSupport = systemSupport;
mUiAutomationService = new UiAutomationService(context, accessibilityServiceInfo, id,
mainHandler, lock, securityPolicy, systemSupport, windowManagerInternal,
globalActionPerfomer);
@@ -153,6 +158,17 @@ class UiAutomationManager {
return mUiAutomationService.mEventTypes;
}
+ int getRelevantEventTypes() {
+ if (mUiAutomationService == null) return 0;
+ return mUiAutomationService.getRelevantEventTypes();
+ }
+
+ @Nullable
+ AccessibilityServiceInfo getServiceInfo() {
+ if (mUiAutomationService == null) return null;
+ return mUiAutomationService.getServiceInfo();
+ }
+
void dumpUiAutomationService(FileDescriptor fd, final PrintWriter pw, String[] args) {
if (mUiAutomationService != null) {
mUiAutomationService.dump(fd, pw, args);
@@ -169,9 +185,10 @@ class UiAutomationManager {
mUiAutomationServiceOwner.unlinkToDeath(mUiAutomationServiceOwnerDeathRecipient, 0);
mUiAutomationServiceOwner = null;
}
+ mSystemSupport.onClientChange(false);
}
- private class UiAutomationService extends AccessibilityClientConnection {
+ private class UiAutomationService extends AbstractAccessibilityServiceConnection {
private final Handler mMainHandler;
UiAutomationService(Context context, AccessibilityServiceInfo accessibilityServiceInfo,
@@ -224,6 +241,17 @@ class UiAutomationManager {
return true;
}
+ @Override
+ public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
+ synchronized (mLock) {
+ pw.append("Ui Automation[eventTypes="
+ + AccessibilityEvent.eventTypeToString(mEventTypes));
+ pw.append(", notificationTimeout=" + mNotificationTimeout);
+ pw.append("]");
+ }
+ }
+
// Since this isn't really an accessibility service, several methods are just stubbed here.
@Override
public boolean setSoftKeyboardShowMode(int mode) {
diff --git a/com/android/server/accounts/AccountManagerService.java b/com/android/server/accounts/AccountManagerService.java
index 0d4f5cb8..31aea638 100644
--- a/com/android/server/accounts/AccountManagerService.java
+++ b/com/android/server/accounts/AccountManagerService.java
@@ -1406,7 +1406,7 @@ public class AccountManagerService
mLocalUnlockedUsers.put(userId, true);
}
if (userId < 1) return;
- syncSharedAccounts(userId);
+ mHandler.post(() -> syncSharedAccounts(userId));
}
private void syncSharedAccounts(int userId) {
@@ -2118,13 +2118,14 @@ public class AccountManagerService
userId));
}
/*
- * Only the system or authenticator should be allowed to remove accounts for that
- * authenticator. This will let users remove accounts (via Settings in the system) but not
- * arbitrary applications (like competing authenticators).
+ * Only the system, authenticator or profile owner should be allowed to remove accounts for
+ * that authenticator. This will let users remove accounts (via Settings in the system) but
+ * not arbitrary applications (like competing authenticators).
*/
UserHandle user = UserHandle.of(userId);
if (!isAccountManagedByCaller(account.type, callingUid, user.getIdentifier())
- && !isSystemUid(callingUid)) {
+ && !isSystemUid(callingUid)
+ && !isProfileOwner(callingUid)) {
String msg = String.format(
"uid %s cannot remove accounts of type: %s",
callingUid,
diff --git a/com/android/server/am/ActiveServices.java b/com/android/server/am/ActiveServices.java
index 3cd2f6ae..2f7d4c1e 100644
--- a/com/android/server/am/ActiveServices.java
+++ b/com/android/server/am/ActiveServices.java
@@ -348,7 +348,7 @@ public final class ActiveServices {
ServiceLookupResult res =
retrieveServiceLocked(service, resolvedType, callingPackage,
- callingPid, callingUid, userId, true, callerFg, false);
+ callingPid, callingUid, userId, true, callerFg, false, false);
if (res == null) {
return null;
}
@@ -597,7 +597,7 @@ public final class ActiveServices {
// If this service is active, make sure it is stopped.
ServiceLookupResult r = retrieveServiceLocked(service, resolvedType, null,
- Binder.getCallingPid(), Binder.getCallingUid(), userId, false, false, false);
+ Binder.getCallingPid(), Binder.getCallingUid(), userId, false, false, false, false);
if (r != null) {
if (r.record != null) {
final long origId = Binder.clearCallingIdentity();
@@ -658,7 +658,7 @@ public final class ActiveServices {
IBinder peekServiceLocked(Intent service, String resolvedType, String callingPackage) {
ServiceLookupResult r = retrieveServiceLocked(service, resolvedType, callingPackage,
Binder.getCallingPid(), Binder.getCallingUid(),
- UserHandle.getCallingUserId(), false, false, false);
+ UserHandle.getCallingUserId(), false, false, false, false);
IBinder ret = null;
if (r != null) {
@@ -1282,12 +1282,19 @@ public final class ActiveServices {
+ ") set BIND_ALLOW_WHITELIST_MANAGEMENT when binding service " + service);
}
+ if ((flags & Context.BIND_ALLOW_INSTANT) != 0 && !isCallerSystem) {
+ throw new SecurityException(
+ "Non-system caller " + caller + " (pid=" + Binder.getCallingPid()
+ + ") set BIND_ALLOW_INSTANT when binding service " + service);
+ }
+
final boolean callerFg = callerApp.setSchedGroup != ProcessList.SCHED_GROUP_BACKGROUND;
final boolean isBindExternal = (flags & Context.BIND_EXTERNAL_SERVICE) != 0;
+ final boolean allowInstant = (flags & Context.BIND_ALLOW_INSTANT) != 0;
ServiceLookupResult res =
retrieveServiceLocked(service, resolvedType, callingPackage, Binder.getCallingPid(),
- Binder.getCallingUid(), userId, true, callerFg, isBindExternal);
+ Binder.getCallingUid(), userId, true, callerFg, isBindExternal, allowInstant);
if (res == null) {
return 0;
}
@@ -1657,7 +1664,8 @@ public final class ActiveServices {
private ServiceLookupResult retrieveServiceLocked(Intent service,
String resolvedType, String callingPackage, int callingPid, int callingUid, int userId,
- boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal) {
+ boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
+ boolean allowInstant) {
ServiceRecord r = null;
if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "retrieveServiceLocked: " + service
+ " type=" + resolvedType + " callingUid=" + callingUid);
@@ -1685,11 +1693,14 @@ public final class ActiveServices {
}
if (r == null) {
try {
+ int flags = ActivityManagerService.STOCK_PM_FLAGS
+ | PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
+ if (allowInstant) {
+ flags |= PackageManager.MATCH_INSTANT;
+ }
// TODO: come back and remove this assumption to triage all services
ResolveInfo rInfo = mAm.getPackageManagerInternalLocked().resolveService(service,
- resolvedType, ActivityManagerService.STOCK_PM_FLAGS
- | PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
- userId, callingUid);
+ resolvedType, flags, userId, callingUid);
ServiceInfo sInfo =
rInfo != null ? rInfo.serviceInfo : null;
if (sInfo == null) {
@@ -2640,7 +2651,7 @@ public final class ActiveServices {
try {
bumpServiceExecutingLocked(s, false, "unbind");
if (b.client != s.app && (c.flags&Context.BIND_WAIVE_PRIORITY) == 0
- && s.app.setProcState <= ActivityManager.PROCESS_STATE_RECEIVER) {
+ && s.app.setProcState <= ActivityManager.PROCESS_STATE_HEAVY_WEIGHT) {
// If this service's process is not already in the cached list,
// then update it in the LRU list here because this may be causing
// it to go down there and we want it to start out near the top.
diff --git a/com/android/server/am/ActivityDisplay.java b/com/android/server/am/ActivityDisplay.java
index b11b16e1..af5cf1ee 100644
--- a/com/android/server/am/ActivityDisplay.java
+++ b/com/android/server/am/ActivityDisplay.java
@@ -65,6 +65,12 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
static final int POSITION_TOP = Integer.MAX_VALUE;
static final int POSITION_BOTTOM = Integer.MIN_VALUE;
+
+ /**
+ * Counter for next free stack ID to use for dynamic activity stacks. Unique across displays.
+ */
+ private static int sNextFreeStackId = 0;
+
private ActivityStackSupervisor mSupervisor;
/** Actual Display this object tracks. */
int mDisplayId;
@@ -231,6 +237,10 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
return getOrCreateStack(windowingMode, activityType, onTop);
}
+ private int getNextStackId() {
+ return sNextFreeStackId++;
+ }
+
/**
* Creates a stack matching the input windowing mode and activity type on this display.
* @param windowingMode The windowing mode the stack should be created in. If
@@ -278,7 +288,7 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
}
}
- final int stackId = mSupervisor.getNextStackId();
+ final int stackId = getNextStackId();
return createStackUnchecked(windowingMode, activityType, stackId, onTop);
}
@@ -399,15 +409,16 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
otherStack.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
}
} finally {
- if (mHomeStack != null && !isTopStack(mHomeStack)) {
+ final ActivityStack topFullscreenStack =
+ getStack(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
+ if (topFullscreenStack != null && mHomeStack != null && !isTopStack(mHomeStack)) {
// Whenever split-screen is dismissed we want the home stack directly behind the
- // currently top stack so it shows up when the top stack is finished.
- final ActivityStack topStack = getTopStack();
+ // current top fullscreen stack so it shows up when the top stack is finished.
// TODO: Would be better to use ActivityDisplay.positionChildAt() for this, however
// ActivityDisplay doesn't have a direct controller to WM side yet. We can switch
// once we have that.
mHomeStack.moveToFront("onSplitScreenModeDismissed");
- topStack.moveToFront("onSplitScreenModeDismissed");
+ topFullscreenStack.moveToFront("onSplitScreenModeDismissed");
}
mSupervisor.mWindowManager.continueSurfaceLayout();
}
@@ -423,7 +434,9 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
|| !otherStack.affectedBySplitScreenResize()) {
continue;
}
- otherStack.setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+ otherStack.setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY,
+ false /* animate */, false /* showRecents */,
+ true /* enteringSplitScreenMode */);
}
} finally {
mSupervisor.mWindowManager.continueSurfaceLayout();
@@ -543,6 +556,16 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
return stack == getTopStack();
}
+ boolean isTopFullscreenStack(ActivityStack stack) {
+ for (int i = mStacks.size() - 1; i >= 0; --i) {
+ final ActivityStack current = mStacks.get(i);
+ if (current.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ return current == stack;
+ }
+ }
+ return false;
+ }
+
int getIndexOf(ActivityStack stack) {
return mStacks.indexOf(stack);
}
diff --git a/com/android/server/am/ActivityManagerConstants.java b/com/android/server/am/ActivityManagerConstants.java
index b52db872..b3a596c8 100644
--- a/com/android/server/am/ActivityManagerConstants.java
+++ b/com/android/server/am/ActivityManagerConstants.java
@@ -66,6 +66,7 @@ final class ActivityManagerConstants extends ContentObserver {
static final String KEY_BG_START_TIMEOUT = "service_bg_start_timeout";
static final String KEY_BOUND_SERVICE_CRASH_RESTART_DURATION = "service_crash_restart_duration";
static final String KEY_BOUND_SERVICE_CRASH_MAX_RETRY = "service_crash_max_retry";
+ static final String KEY_PROCESS_START_ASYNC = "process_start_async";
private static final int DEFAULT_MAX_CACHED_PROCESSES = 32;
private static final long DEFAULT_BACKGROUND_SETTLE_TIME = 60*1000;
@@ -93,6 +94,7 @@ final class ActivityManagerConstants extends ContentObserver {
private static final long DEFAULT_BG_START_TIMEOUT = 15*1000;
private static final long DEFAULT_BOUND_SERVICE_CRASH_RESTART_DURATION = 30*60_000;
private static final int DEFAULT_BOUND_SERVICE_CRASH_MAX_RETRY = 16;
+ private static final boolean DEFAULT_PROCESS_START_ASYNC = true;
// Maximum number of cached processes we will allow.
@@ -202,6 +204,9 @@ final class ActivityManagerConstants extends ContentObserver {
// Maximum number of retries for bound foreground services that crash soon after start
public long BOUND_SERVICE_MAX_CRASH_RETRY = DEFAULT_BOUND_SERVICE_CRASH_MAX_RETRY;
+ // Indicates if the processes need to be started asynchronously.
+ public boolean FLAG_PROCESS_START_ASYNC = DEFAULT_PROCESS_START_ASYNC;
+
private final ActivityManagerService mService;
private ContentResolver mResolver;
private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -325,6 +330,8 @@ final class ActivityManagerConstants extends ContentObserver {
DEFAULT_BOUND_SERVICE_CRASH_RESTART_DURATION);
BOUND_SERVICE_MAX_CRASH_RETRY = mParser.getInt(KEY_BOUND_SERVICE_CRASH_MAX_RETRY,
DEFAULT_BOUND_SERVICE_CRASH_MAX_RETRY);
+ FLAG_PROCESS_START_ASYNC = mParser.getBoolean(KEY_PROCESS_START_ASYNC,
+ DEFAULT_PROCESS_START_ASYNC);
updateMaxCachedProcesses();
}
diff --git a/com/android/server/am/ActivityManagerService.java b/com/android/server/am/ActivityManagerService.java
index fe992daf..d92b3b86 100644
--- a/com/android/server/am/ActivityManagerService.java
+++ b/com/android/server/am/ActivityManagerService.java
@@ -34,6 +34,7 @@ import static android.app.ActivityManagerInternal.ASSIST_KEY_CONTENT;
import static android.app.ActivityManagerInternal.ASSIST_KEY_DATA;
import static android.app.ActivityManagerInternal.ASSIST_KEY_RECEIVER_EXTRAS;
import static android.app.ActivityManagerInternal.ASSIST_KEY_STRUCTURE;
+import static android.app.ActivityThread.PROC_START_SEQ_IDENT;
import static android.app.AppOpsManager.OP_ASSIST_STRUCTURE;
import static android.app.AppOpsManager.OP_NONE;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -44,7 +45,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS;
import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
import static android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY;
@@ -208,6 +211,7 @@ import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityManager.StackInfo;
import android.app.ActivityManager.TaskSnapshot;
import android.app.ActivityManagerInternal;
+import android.app.ActivityManagerInternal.ScreenObserver;
import android.app.ActivityManagerInternal.SleepToken;
import android.app.ActivityOptions;
import android.app.ActivityThread;
@@ -350,6 +354,7 @@ import android.text.style.SuggestionSpan;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
+import android.util.LongSparseArray;
import android.util.StatsLog;
import android.util.TimingsTraceLog;
import android.util.DebugUtils;
@@ -386,6 +391,7 @@ import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.BatteryStatsImpl;
import com.android.internal.os.BinderInternal;
+import com.android.internal.os.ByteTransferPipe;
import com.android.internal.os.IResultReceiver;
import com.android.internal.os.ProcessCpuTracker;
import com.android.internal.os.TransferPipe;
@@ -398,6 +404,7 @@ import com.android.internal.util.FastPrintWriter;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.MemInfoReader;
import com.android.internal.util.Preconditions;
+import com.android.server.AlarmManagerInternal;
import com.android.server.AppOpsService;
import com.android.server.AttributeCache;
import com.android.server.DeviceIdleController;
@@ -414,9 +421,11 @@ import com.android.server.SystemServiceManager;
import com.android.server.ThreadPriorityBooster;
import com.android.server.Watchdog;
import com.android.server.am.ActivityStack.ActivityState;
+import com.android.server.am.EventLogTags;
import com.android.server.am.proto.ActivityManagerServiceProto;
import com.android.server.am.proto.BroadcastProto;
import com.android.server.am.proto.GrantUriProto;
+import com.android.server.am.proto.MemInfoProto;
import com.android.server.am.proto.NeededUriGrantsProto;
import com.android.server.am.proto.StickyBroadcastProto;
import com.android.server.firewall.IntentFirewall;
@@ -466,6 +475,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import dalvik.system.VMRuntime;
+
import libcore.io.IoUtils;
import libcore.util.EmptyArray;
@@ -623,7 +633,13 @@ public class ActivityManagerService extends IActivityManager.Stub
/** All system services */
SystemServiceManager mSystemServiceManager;
- AssistUtils mAssistUtils;
+
+ // Wrapper around VoiceInteractionServiceManager
+ private AssistUtils mAssistUtils;
+
+ // Keeps track of the active voice interaction service component, notified from
+ // VoiceInteractionManagerService
+ ComponentName mActiveVoiceInteractionServiceComponent;
private Installer mInstaller;
@@ -631,7 +647,7 @@ public class ActivityManagerService extends IActivityManager.Stub
final ActivityStackSupervisor mStackSupervisor;
private final KeyguardController mKeyguardController;
- final ActivityStarter mActivityStarter;
+ private final ActivityStartController mActivityStartController;
final ClientLifecycleManager mLifecycleManager;
@@ -1360,7 +1376,7 @@ public class ActivityManagerService extends IActivityManager.Stub
@GuardedBy("this") boolean mCallFinishBooting = false;
@GuardedBy("this") boolean mBootAnimationComplete = false;
@GuardedBy("this") boolean mLaunchWarningShown = false;
- @GuardedBy("this") boolean mCheckedForSetup = false;
+ private @GuardedBy("this") boolean mCheckedForSetup = false;
final Context mContext;
@@ -1583,6 +1599,20 @@ public class ActivityManagerService extends IActivityManager.Stub
@VisibleForTesting
long mProcStateSeqCounter = 0;
+ /**
+ * A global counter for generating sequence numbers to uniquely identify pending process starts.
+ */
+ @GuardedBy("this")
+ private long mProcStartSeqCounter = 0;
+
+ /**
+ * Contains {@link ProcessRecord} objects for pending process starts.
+ *
+ * Mapping: {@link #mProcStartSeqCounter} -> {@link ProcessRecord}
+ */
+ @GuardedBy("this")
+ private final LongSparseArray<ProcessRecord> mPendingStarts = new LongSparseArray<>();
+
private final Injector mInjector;
static final class ProcessChangeItem {
@@ -1615,6 +1645,8 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
+ final List<ScreenObserver> mScreenObservers = new ArrayList<>();
+
final RemoteCallbackList<IProcessObserver> mProcessObservers = new RemoteCallbackList<>();
ProcessChangeItem[] mActiveProcessChanges = new ProcessChangeItem[5];
@@ -1706,7 +1738,6 @@ public class ActivityManagerService extends IActivityManager.Stub
static final int SHOW_UID_ERROR_UI_MSG = 14;
static final int SHOW_FINGERPRINT_ERROR_UI_MSG = 15;
static final int PROC_START_TIMEOUT_MSG = 20;
- static final int DO_PENDING_ACTIVITY_LAUNCHES_MSG = 21;
static final int KILL_APPLICATION_MSG = 22;
static final int FINALIZE_PENDING_INTENT_MSG = 23;
static final int POST_HEAVY_NOTIFICATION_MSG = 24;
@@ -1738,13 +1769,13 @@ public class ActivityManagerService extends IActivityManager.Stub
static final int LOG_STACK_STATE = 60;
static final int VR_MODE_CHANGE_MSG = 61;
static final int HANDLE_TRUST_STORAGE_UPDATE_MSG = 63;
- static final int NOTIFY_VR_SLEEPING_MSG = 65;
+ static final int DISPATCH_SCREEN_AWAKE_MSG = 64;
+ static final int DISPATCH_SCREEN_KEYGUARD_MSG = 65;
static final int SERVICE_FOREGROUND_TIMEOUT_MSG = 66;
static final int DISPATCH_PENDING_INTENT_CANCEL_MSG = 67;
static final int PUSH_TEMP_WHITELIST_UI_MSG = 68;
static final int SERVICE_FOREGROUND_CRASH_MSG = 69;
static final int DISPATCH_OOM_ADJ_OBSERVER_MSG = 70;
- static final int NOTIFY_VR_KEYGUARD_MSG = 74;
static final int FIRST_ACTIVITY_STACK_MSG = 100;
static final int FIRST_BROADCAST_QUEUE_MSG = 200;
@@ -1771,6 +1802,8 @@ public class ActivityManagerService extends IActivityManager.Stub
final ServiceThread mHandlerThread;
final MainHandler mHandler;
final Handler mUiHandler;
+ final ServiceThread mProcStartHandlerThread;
+ final Handler mProcStartHandler;
final ActivityManagerConstants mConstants;
@@ -2076,11 +2109,6 @@ public class ActivityManagerService extends IActivityManager.Stub
processContentProviderPublishTimedOutLocked(app);
}
} break;
- case DO_PENDING_ACTIVITY_LAUNCHES_MSG: {
- synchronized (ActivityManagerService.this) {
- mActivityStarter.doPendingActivityLaunchesLocked(true);
- }
- } break;
case KILL_APPLICATION_MSG: {
synchronized (ActivityManagerService.this) {
final int appId = msg.arg1;
@@ -2385,11 +2413,17 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
} break;
- case NOTIFY_VR_SLEEPING_MSG: {
- notifyVrManagerOfSleepState(msg.arg1 != 0);
+ case DISPATCH_SCREEN_AWAKE_MSG: {
+ final boolean isAwake = msg.arg1 != 0;
+ for (int i = mScreenObservers.size() - 1; i >= 0; i--) {
+ mScreenObservers.get(i).onAwakeStateChanged(isAwake);
+ }
} break;
- case NOTIFY_VR_KEYGUARD_MSG: {
- notifyVrManagerOfKeyguardState(msg.arg1 != 0);
+ case DISPATCH_SCREEN_KEYGUARD_MSG: {
+ final boolean isShowing = msg.arg1 != 0;
+ for (int i = mScreenObservers.size() - 1; i >= 0; i--) {
+ mScreenObservers.get(i).onKeyguardStateChanged(isShowing);
+ }
} break;
case HANDLE_TRUST_STORAGE_UPDATE_MSG: {
synchronized (ActivityManagerService.this) {
@@ -2569,14 +2603,13 @@ public class ActivityManagerService extends IActivityManager.Stub
@Override
public void dumpHigh(FileDescriptor fd, PrintWriter pw, String[] args,
boolean asProto) {
- if (asProto) return;
- mActivityManagerService.dumpApplicationMemoryUsage(fd,
- pw, " ", new String[] {"-a"}, false, null);
+ dump(fd, pw, new String[] {"-a"}, asProto);
}
+
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args, boolean asProto) {
- if (asProto) return;
- mActivityManagerService.dumpApplicationMemoryUsage(fd, pw, " ", args, false, null);
+ mActivityManagerService.dumpApplicationMemoryUsage(
+ fd, pw, " ", args, false, null, asProto);
}
};
@@ -2677,7 +2710,7 @@ public class ActivityManagerService extends IActivityManager.Stub
mContext = mInjector.getContext();
mUiContext = null;
GL_ES_VERSION = 0;
- mActivityStarter = null;
+ mActivityStartController = null;
mAppErrors = null;
mAppWarnings = null;
mAppOpsService = mInjector.getAppOpsService(null, null);
@@ -2703,6 +2736,8 @@ public class ActivityManagerService extends IActivityManager.Stub
mVrController = null;
mLockTaskController = null;
mLifecycleManager = null;
+ mProcStartHandlerThread = null;
+ mProcStartHandler = null;
}
// Note: This method is invoked on the main thread but may need to attach various
@@ -2727,6 +2762,11 @@ public class ActivityManagerService extends IActivityManager.Stub
mHandler = new MainHandler(mHandlerThread.getLooper());
mUiHandler = mInjector.getUiHandler(this);
+ mProcStartHandlerThread = new ServiceThread(TAG + ":procStart",
+ THREAD_PRIORITY_FOREGROUND, false /* allowIo */);
+ mProcStartHandlerThread.start();
+ mProcStartHandler = new Handler(mProcStartHandlerThread.getLooper());
+
mConstants = new ActivityManagerConstants(this, mHandler);
/* static; one-time init here */
@@ -2801,7 +2841,7 @@ public class ActivityManagerService extends IActivityManager.Stub
mIntentFirewall = new IntentFirewall(new IntentFirewallInterface(), mHandler);
mTaskChangeNotificationController =
new TaskChangeNotificationController(this, mStackSupervisor, mHandler);
- mActivityStarter = new ActivityStarter(this);
+ mActivityStartController = new ActivityStartController(this);
mRecentTasks = createRecentTasks();
mStackSupervisor.setRecentTasks(mRecentTasks);
mLockTaskController = new LockTaskController(mContext, mStackSupervisor, mHandler);
@@ -2942,9 +2982,11 @@ public class ActivityManagerService extends IActivityManager.Stub
try {
return super.onTransact(code, data, reply, flags);
} catch (RuntimeException e) {
- // The activity manager only throws security exceptions, so let's
+ // The activity manager only throws certain exceptions intentionally, so let's
// log all others.
- if (!(e instanceof SecurityException)) {
+ if (!(e instanceof SecurityException
+ || e instanceof IllegalArgumentException
+ || e instanceof IllegalStateException)) {
Slog.wtf(TAG, "Activity Manager Crash."
+ " UID:" + Binder.getCallingUid()
+ " PID:" + Binder.getCallingPid()
@@ -3287,32 +3329,6 @@ public class ActivityManagerService extends IActivityManager.Stub
mHandler.obtainMessage(VR_MODE_CHANGE_MSG, 0, 0, r));
}
- private void sendNotifyVrManagerOfSleepState(boolean isSleeping) {
- mHandler.sendMessage(
- mHandler.obtainMessage(NOTIFY_VR_SLEEPING_MSG, isSleeping ? 1 : 0, 0));
- }
-
- private void notifyVrManagerOfSleepState(boolean isSleeping) {
- final VrManagerInternal vrService = LocalServices.getService(VrManagerInternal.class);
- if (vrService == null) {
- return;
- }
- vrService.onSleepStateChanged(isSleeping);
- }
-
- private void sendNotifyVrManagerOfKeyguardState(boolean isShowing) {
- mHandler.sendMessage(
- mHandler.obtainMessage(NOTIFY_VR_KEYGUARD_MSG, isShowing ? 1 : 0, 0));
- }
-
- private void notifyVrManagerOfKeyguardState(boolean isShowing) {
- final VrManagerInternal vrService = LocalServices.getService(VrManagerInternal.class);
- if (vrService == null) {
- return;
- }
- vrService.onKeyguardStateChanged(isShowing);
- }
-
final void showAskCompatModeDialogLocked(ActivityRecord r) {
Message msg = Message.obtain();
msg.what = SHOW_COMPAT_MODE_DIALOG_UI_MSG;
@@ -3386,8 +3402,12 @@ public class ActivityManagerService extends IActivityManager.Stub
if (lrui >= 0) {
if (!app.killed) {
Slog.wtfStack(TAG, "Removing process that hasn't been killed: " + app);
- killProcessQuiet(app.pid);
- killProcessGroup(app.uid, app.pid);
+ if (app.pid > 0) {
+ killProcessQuiet(app.pid);
+ killProcessGroup(app.uid, app.pid);
+ } else {
+ app.pendingStart = false;
+ }
}
if (lrui <= mLruProcessActivityStart) {
mLruProcessActivityStart--;
@@ -3648,7 +3668,7 @@ public class ActivityManagerService extends IActivityManager.Stub
|| transit == TRANSIT_TASK_TO_FRONT;
}
- int startIsolatedProcess(String entryPoint, String[] entryPointArgs,
+ boolean startIsolatedProcess(String entryPoint, String[] entryPointArgs,
String processName, String abiOverride, int uid, Runnable crashHandler) {
synchronized(this) {
ApplicationInfo info = new ApplicationInfo();
@@ -3670,7 +3690,7 @@ public class ActivityManagerService extends IActivityManager.Stub
null /* hostingName */, true /* allowWhileBooting */, true /* isolated */,
uid, true /* keepIfLarge */, abiOverride, entryPoint, entryPointArgs,
crashHandler);
- return proc != null ? proc.pid : 0;
+ return proc != null;
}
}
@@ -3791,9 +3811,9 @@ public class ActivityManagerService extends IActivityManager.Stub
}
checkTime(startTime, "startProcess: stepping in to startProcess");
- startProcessLocked(app, hostingType, hostingNameStr, abiOverride);
+ final boolean success = startProcessLocked(app, hostingType, hostingNameStr, abiOverride);
checkTime(startTime, "startProcess: done starting proc!");
- return (app.pid != 0) ? app : null;
+ return success ? app : null;
}
boolean isAllowedWhileBooting(ApplicationInfo ai) {
@@ -3805,8 +3825,14 @@ public class ActivityManagerService extends IActivityManager.Stub
startProcessLocked(app, hostingType, hostingNameStr, null /* abiOverride */);
}
- private final void startProcessLocked(ProcessRecord app, String hostingType,
+ /**
+ * @return {@code true} if process start is successful, false otherwise.
+ */
+ private final boolean startProcessLocked(ProcessRecord app, String hostingType,
String hostingNameStr, String abiOverride) {
+ if (app.pendingStart) {
+ return true;
+ }
long startTime = SystemClock.elapsedRealtime();
if (app.pid > 0 && app.pid != MY_PID) {
checkTime(startTime, "startProcess: removing from pids map");
@@ -3962,102 +3988,235 @@ public class ActivityManagerService extends IActivityManager.Stub
// Start the process. It will either succeed and return a result containing
// the PID of the new process, or else throw a RuntimeException.
final String entryPoint = "android.app.ActivityThread";
+
+ return startProcessLocked(hostingType, hostingNameStr, entryPoint, app, uid, gids,
+ runtimeFlags, mountExternal, seInfo, requiredAbi, instructionSet, invokeWith,
+ startTime);
+ } catch (RuntimeException e) {
+ Slog.e(TAG, "Failure starting process " + app.processName, e);
+
+ // Something went very wrong while trying to start this process; one
+ // common case is when the package is frozen due to an active
+ // upgrade. To recover, clean up any active bookkeeping related to
+ // starting this process. (We already invoked this method once when
+ // the package was initially frozen through KILL_APPLICATION_MSG, so
+ // it doesn't hurt to use it again.)
+ forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid), false,
+ false, true, false, false, UserHandle.getUserId(app.userId), "start failure");
+ return false;
+ }
+ }
+
+ @GuardedBy("this")
+ private boolean startProcessLocked(String hostingType, String hostingNameStr, String entryPoint,
+ ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
+ String seInfo, String requiredAbi, String instructionSet, String invokeWith,
+ long startTime) {
+ app.pendingStart = true;
+ app.killedByAm = false;
+ app.removed = false;
+ app.killed = false;
+ final long startSeq = app.startSeq = ++mProcStartSeqCounter;
+ app.setStartParams(uid, hostingType, hostingNameStr, seInfo, startTime);
+ if (mConstants.FLAG_PROCESS_START_ASYNC) {
+ if (DEBUG_PROCESSES) Slog.i(TAG_PROCESSES,
+ "Posting procStart msg for " + app.toShortString());
+ mProcStartHandler.post(() -> {
+ try {
+ synchronized (ActivityManagerService.this) {
+ final String reason = isProcStartValidLocked(app, startSeq);
+ if (reason != null) {
+ Slog.w(TAG_PROCESSES, app + " not valid anymore,"
+ + " don't start process, " + reason);
+ app.pendingStart = false;
+ return;
+ }
+ app.usingWrapper = invokeWith != null
+ || SystemProperties.get("wrap." + app.processName) != null;
+ mPendingStarts.put(startSeq, app);
+ }
+ final ProcessStartResult startResult = startProcess(app.hostingType, entryPoint,
+ app, app.startUid, gids, runtimeFlags, mountExternal, app.seInfo,
+ requiredAbi, instructionSet, invokeWith, app.startTime);
+ synchronized (ActivityManagerService.this) {
+ handleProcessStartedLocked(app, startResult, startSeq);
+ }
+ } catch (RuntimeException e) {
+ synchronized (ActivityManagerService.this) {
+ Slog.e(TAG, "Failure starting process " + app.processName, e);
+ mPendingStarts.remove(startSeq);
+ app.pendingStart = false;
+ forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid),
+ false, false, true, false, false,
+ UserHandle.getUserId(app.userId), "start failure");
+ }
+ }
+ });
+ return true;
+ } else {
+ try {
+ final ProcessStartResult startResult = startProcess(hostingType, entryPoint, app,
+ uid, gids, runtimeFlags, mountExternal, seInfo, requiredAbi, instructionSet,
+ invokeWith, startTime);
+ handleProcessStartedLocked(app, startResult.pid, startResult.usingWrapper,
+ startSeq, false);
+ } catch (RuntimeException e) {
+ Slog.e(TAG, "Failure starting process " + app.processName, e);
+ app.pendingStart = false;
+ forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid),
+ false, false, true, false, false,
+ UserHandle.getUserId(app.userId), "start failure");
+ }
+ return app.pid > 0;
+ }
+ }
+
+ private ProcessStartResult startProcess(String hostingType, String entryPoint,
+ ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
+ String seInfo, String requiredAbi, String instructionSet, String invokeWith,
+ long startTime) {
+ try {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " +
app.processName);
checkTime(startTime, "startProcess: asking zygote to start proc");
- ProcessStartResult startResult;
+ final ProcessStartResult startResult;
if (hostingType.equals("webview_service")) {
startResult = startWebView(entryPoint,
app.processName, uid, uid, gids, runtimeFlags, mountExternal,
app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
- app.info.dataDir, null, null);
+ app.info.dataDir, null,
+ new String[] {PROC_START_SEQ_IDENT + app.startSeq});
} else {
startResult = Process.start(entryPoint,
app.processName, uid, uid, gids, runtimeFlags, mountExternal,
app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
- app.info.dataDir, invokeWith, null);
+ app.info.dataDir, invokeWith,
+ new String[] {PROC_START_SEQ_IDENT + app.startSeq});
}
checkTime(startTime, "startProcess: returned from zygote!");
+ return startResult;
+ } finally {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ }
+ }
- mBatteryStatsService.noteProcessStart(app.processName, app.info.uid);
- checkTime(startTime, "startProcess: done updating battery stats");
-
- EventLog.writeEvent(EventLogTags.AM_PROC_START,
- UserHandle.getUserId(uid), startResult.pid, uid,
- app.processName, hostingType,
- hostingNameStr != null ? hostingNameStr : "");
+ @GuardedBy("this")
+ private String isProcStartValidLocked(ProcessRecord app, long expectedStartSeq) {
+ StringBuilder sb = null;
+ if (app.killedByAm) {
+ if (sb == null) sb = new StringBuilder();
+ sb.append("killedByAm=true;");
+ }
+ if (mProcessNames.get(app.processName, app.uid) != app) {
+ if (sb == null) sb = new StringBuilder();
+ sb.append("No entry in mProcessNames;");
+ }
+ if (!app.pendingStart) {
+ if (sb == null) sb = new StringBuilder();
+ sb.append("pendingStart=false;");
+ }
+ if (app.startSeq > expectedStartSeq) {
+ if (sb == null) sb = new StringBuilder();
+ sb.append("seq=" + app.startSeq + ",expected=" + expectedStartSeq + ";");
+ }
+ return sb == null ? null : sb.toString();
+ }
- try {
- AppGlobals.getPackageManager().logAppProcessStartIfNeeded(app.processName, app.uid,
- seInfo, app.info.sourceDir, startResult.pid);
- } catch (RemoteException ex) {
- // Ignore
+ @GuardedBy("this")
+ private boolean handleProcessStartedLocked(ProcessRecord pending,
+ ProcessStartResult startResult, long expectedStartSeq) {
+ // Indicates that this process start has been taken care of.
+ if (mPendingStarts.get(expectedStartSeq) == null) {
+ if (pending.pid == startResult.pid) {
+ pending.usingWrapper = startResult.usingWrapper;
+ // TODO: Update already existing clients of usingWrapper
}
+ return false;
+ }
+ return handleProcessStartedLocked(pending, startResult.pid, startResult.usingWrapper,
+ expectedStartSeq, false);
+ }
- if (app.persistent) {
- Watchdog.getInstance().processStarted(app.processName, startResult.pid);
- }
+ @GuardedBy("this")
+ private boolean handleProcessStartedLocked(ProcessRecord app, int pid, boolean usingWrapper,
+ long expectedStartSeq, boolean procAttached) {
+ mPendingStarts.remove(expectedStartSeq);
+ final String reason = isProcStartValidLocked(app, expectedStartSeq);
+ if (reason != null) {
+ Slog.w(TAG_PROCESSES, app + " start not valid, killing pid=" + pid
+ + ", " + reason);
+ app.pendingStart = false;
+ Process.killProcessQuiet(pid);
+ Process.killProcessGroup(app.uid, app.pid);
+ return false;
+ }
+ mBatteryStatsService.noteProcessStart(app.processName, app.info.uid);
+ checkTime(app.startTime, "startProcess: done updating battery stats");
- checkTime(startTime, "startProcess: building log message");
- StringBuilder buf = mStringBuilder;
- buf.setLength(0);
- buf.append("Start proc ");
- buf.append(startResult.pid);
- buf.append(':');
- buf.append(app.processName);
- buf.append('/');
- UserHandle.formatUid(buf, uid);
- if (app.isolatedEntryPoint != null) {
- buf.append(" [");
- buf.append(app.isolatedEntryPoint);
- buf.append("]");
- }
- buf.append(" for ");
- buf.append(hostingType);
- if (hostingNameStr != null) {
- buf.append(" ");
- buf.append(hostingNameStr);
- }
- Slog.i(TAG, buf.toString());
- app.setPid(startResult.pid);
- app.usingWrapper = startResult.usingWrapper;
- app.removed = false;
- app.killed = false;
- app.killedByAm = false;
- checkTime(startTime, "startProcess: starting to update pids map");
- ProcessRecord oldApp;
- synchronized (mPidsSelfLocked) {
- oldApp = mPidsSelfLocked.get(startResult.pid);
- }
- // If there is already an app occupying that pid that hasn't been cleaned up
- if (oldApp != null && !app.isolated) {
- // Clean up anything relating to this pid first
- Slog.w(TAG, "Reusing pid " + startResult.pid
- + " while app is still mapped to it");
- cleanUpApplicationRecordLocked(oldApp, false, false, -1,
- true /*replacingPid*/);
- }
- synchronized (mPidsSelfLocked) {
- this.mPidsSelfLocked.put(startResult.pid, app);
+ EventLog.writeEvent(EventLogTags.AM_PROC_START,
+ UserHandle.getUserId(app.startUid), pid, app.startUid,
+ app.processName, app.hostingType,
+ app.hostingNameStr != null ? app.hostingNameStr : "");
+
+ try {
+ AppGlobals.getPackageManager().logAppProcessStartIfNeeded(app.processName, app.uid,
+ app.seInfo, app.info.sourceDir, pid);
+ } catch (RemoteException ex) {
+ // Ignore
+ }
+
+ if (app.persistent) {
+ Watchdog.getInstance().processStarted(app.processName, pid);
+ }
+
+ checkTime(app.startTime, "startProcess: building log message");
+ StringBuilder buf = mStringBuilder;
+ buf.setLength(0);
+ buf.append("Start proc ");
+ buf.append(pid);
+ buf.append(':');
+ buf.append(app.processName);
+ buf.append('/');
+ UserHandle.formatUid(buf, app.startUid);
+ if (app.isolatedEntryPoint != null) {
+ buf.append(" [");
+ buf.append(app.isolatedEntryPoint);
+ buf.append("]");
+ }
+ buf.append(" for ");
+ buf.append(app.hostingType);
+ if (app.hostingNameStr != null) {
+ buf.append(" ");
+ buf.append(app.hostingNameStr);
+ }
+ Slog.i(TAG, buf.toString());
+ app.setPid(pid);
+ app.usingWrapper = usingWrapper;
+ app.pendingStart = false;
+ checkTime(app.startTime, "startProcess: starting to update pids map");
+ ProcessRecord oldApp;
+ synchronized (mPidsSelfLocked) {
+ oldApp = mPidsSelfLocked.get(pid);
+ }
+ // If there is already an app occupying that pid that hasn't been cleaned up
+ if (oldApp != null && !app.isolated) {
+ // Clean up anything relating to this pid first
+ Slog.w(TAG, "Reusing pid " + pid
+ + " while app is still mapped to it");
+ cleanUpApplicationRecordLocked(oldApp, false, false, -1,
+ true /*replacingPid*/);
+ }
+ synchronized (mPidsSelfLocked) {
+ this.mPidsSelfLocked.put(pid, app);
+ if (!procAttached) {
Message msg = mHandler.obtainMessage(PROC_START_TIMEOUT_MSG);
msg.obj = app;
- mHandler.sendMessageDelayed(msg, startResult.usingWrapper
+ mHandler.sendMessageDelayed(msg, usingWrapper
? PROC_START_TIMEOUT_WITH_WRAPPER : PROC_START_TIMEOUT);
}
- checkTime(startTime, "startProcess: done updating pids map");
- } catch (RuntimeException e) {
- Slog.e(TAG, "Failure starting process " + app.processName, e);
-
- // Something went very wrong while trying to start this process; one
- // common case is when the package is frozen due to an active
- // upgrade. To recover, clean up any active bookkeeping related to
- // starting this process. (We already invoked this method once when
- // the package was initially frozen through KILL_APPLICATION_MSG, so
- // it doesn't hurt to use it again.)
- forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid), false,
- false, true, false, false, UserHandle.getUserId(app.userId), "start failure");
}
+ checkTime(app.startTime, "startProcess: done updating pids map");
+ return true;
}
void updateUsageStats(ActivityRecord component, boolean resumed) {
@@ -4121,7 +4280,7 @@ public class ActivityManagerService extends IActivityManager.Stub
// For ANR debugging to verify if the user activity is the one that actually
// launched.
final String myReason = reason + ":" + userId + ":" + resolvedUserId;
- mActivityStarter.startHomeActivityLocked(intent, aInfo, myReason);
+ mActivityStartController.startHomeActivity(intent, aInfo, myReason);
}
} else {
Slog.wtf(TAG, "No home screen found for " + intent, new Throwable());
@@ -4154,49 +4313,12 @@ public class ActivityManagerService extends IActivityManager.Stub
return ai;
}
- /**
- * Starts the "new version setup screen" if appropriate.
- */
- void startSetupActivityLocked() {
- // Only do this once per boot.
- if (mCheckedForSetup) {
- return;
- }
+ boolean getCheckedForSetup() {
+ return mCheckedForSetup;
+ }
- // We will show this screen if the current one is a different
- // version than the last one shown, and we are not running in
- // low-level factory test mode.
- final ContentResolver resolver = mContext.getContentResolver();
- if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL &&
- Settings.Global.getInt(resolver,
- Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
- mCheckedForSetup = true;
-
- // See if we should be showing the platform update setup UI.
- final Intent intent = new Intent(Intent.ACTION_UPGRADE_SETUP);
- final List<ResolveInfo> ris = mContext.getPackageManager().queryIntentActivities(intent,
- PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA);
- if (!ris.isEmpty()) {
- final ResolveInfo ri = ris.get(0);
- String vers = ri.activityInfo.metaData != null
- ? ri.activityInfo.metaData.getString(Intent.METADATA_SETUP_VERSION)
- : null;
- if (vers == null && ri.activityInfo.applicationInfo.metaData != null) {
- vers = ri.activityInfo.applicationInfo.metaData.getString(
- Intent.METADATA_SETUP_VERSION);
- }
- String lastVers = Settings.Secure.getString(
- resolver, Settings.Secure.LAST_SETUP_SHOWN);
- if (vers != null && !vers.equals(lastVers)) {
- intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
- intent.setComponent(new ComponentName(
- ri.activityInfo.packageName, ri.activityInfo.name));
- mActivityStarter.startActivityLocked(null, intent, null /*ephemeralIntent*/,
- null, ri.activityInfo, null /*rInfo*/, null, null, null, null, 0, 0, 0,
- null, 0, 0, 0, null, false, false, null, null, "startSetupActivity");
- }
- }
- }
+ void setCheckedForSetup(boolean checked) {
+ mCheckedForSetup = checked;
}
CompatibilityInfo compatibilityInfoForPackageLocked(ApplicationInfo ai) {
@@ -4562,9 +4684,18 @@ public class ActivityManagerService extends IActivityManager.Stub
userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
userId, false, ALLOW_FULL_ONLY, "startActivity", null);
// TODO: Switch to user app stacks here.
- return mActivityStarter.startActivityMayWait(caller, -1, callingPackage, intent,
- resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
- profilerInfo, null, null, bOptions, false, userId, null, "startActivityAsUser");
+ return mActivityStartController.obtainStarter(intent, "startActivityAsUser")
+ .setCaller(caller)
+ .setCallingPackage(callingPackage)
+ .setResolvedType(resolvedType)
+ .setResultTo(resultTo)
+ .setResultWho(resultWho)
+ .setRequestCode(requestCode)
+ .setStartFlags(startFlags)
+ .setProfilerInfo(profilerInfo)
+ .setMayWait(bOptions, userId)
+ .execute();
+
}
@Override
@@ -4625,11 +4756,17 @@ public class ActivityManagerService extends IActivityManager.Stub
// TODO: Switch to user app stacks here.
try {
- int ret = mActivityStarter.startActivityMayWait(null, targetUid, targetPackage, intent,
- resolvedType, null, null, resultTo, resultWho, requestCode, startFlags, null,
- null, null, bOptions, ignoreTargetSecurity, userId, null,
- "startActivityAsCaller");
- return ret;
+ return mActivityStartController.obtainStarter(intent, "startActivityAsCaller")
+ .setCallingUid(targetUid)
+ .setCallingPackage(targetPackage)
+ .setResolvedType(resolvedType)
+ .setResultTo(resultTo)
+ .setResultWho(resultWho)
+ .setRequestCode(requestCode)
+ .setStartFlags(startFlags)
+ .setMayWait(bOptions, userId)
+ .setIgnoreTargetSecurity(ignoreTargetSecurity)
+ .execute();
} catch (SecurityException e) {
// XXX need to figure out how to propagate to original app.
// A SecurityException here is generally actually a fault of the original
@@ -4655,9 +4792,18 @@ public class ActivityManagerService extends IActivityManager.Stub
userId, false, ALLOW_FULL_ONLY, "startActivityAndWait", null);
WaitResult res = new WaitResult();
// TODO: Switch to user app stacks here.
- mActivityStarter.startActivityMayWait(caller, -1, callingPackage, intent, resolvedType,
- null, null, resultTo, resultWho, requestCode, startFlags, profilerInfo, res, null,
- bOptions, false, userId, null, "startActivityAndWait");
+ mActivityStartController.obtainStarter(intent, "startActivityAndWait")
+ .setCaller(caller)
+ .setCallingPackage(callingPackage)
+ .setResolvedType(resolvedType)
+ .setResultTo(resultTo)
+ .setResultWho(resultWho)
+ .setRequestCode(requestCode)
+ .setStartFlags(startFlags)
+ .setMayWait(bOptions, userId)
+ .setProfilerInfo(profilerInfo)
+ .setWaitResult(res)
+ .execute();
return res;
}
@@ -4669,10 +4815,17 @@ public class ActivityManagerService extends IActivityManager.Stub
userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
userId, false, ALLOW_FULL_ONLY, "startActivityWithConfig", null);
// TODO: Switch to user app stacks here.
- int ret = mActivityStarter.startActivityMayWait(caller, -1, callingPackage, intent,
- resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
- null, null, config, bOptions, false, userId, null, "startActivityWithConfig");
- return ret;
+ return mActivityStartController.obtainStarter(intent, "startActivityWithConfig")
+ .setCaller(caller)
+ .setCallingPackage(callingPackage)
+ .setResolvedType(resolvedType)
+ .setResultTo(resultTo)
+ .setResultWho(resultWho)
+ .setRequestCode(requestCode)
+ .setStartFlags(startFlags)
+ .setGlobalConfiguration(config)
+ .setMayWait(bOptions, userId)
+ .execute();
}
@Override
@@ -4718,9 +4871,16 @@ public class ActivityManagerService extends IActivityManager.Stub
userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, false,
ALLOW_FULL_ONLY, "startVoiceActivity", null);
// TODO: Switch to user app stacks here.
- return mActivityStarter.startActivityMayWait(null, callingUid, callingPackage, intent,
- resolvedType, session, interactor, null, null, 0, startFlags, profilerInfo, null,
- null, bOptions, false, userId, null, "startVoiceActivity");
+ return mActivityStartController.obtainStarter(intent, "startVoiceActivity")
+ .setCallingUid(callingUid)
+ .setCallingPackage(callingPackage)
+ .setResolvedType(resolvedType)
+ .setVoiceSession(session)
+ .setVoiceInteractor(interactor)
+ .setStartFlags(startFlags)
+ .setProfilerInfo(profilerInfo)
+ .setMayWait(bOptions, userId)
+ .execute();
}
@Override
@@ -4729,9 +4889,13 @@ public class ActivityManagerService extends IActivityManager.Stub
enforceCallingPermission(BIND_VOICE_INTERACTION, "startAssistantActivity()");
userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, false,
ALLOW_FULL_ONLY, "startAssistantActivity", null);
- return mActivityStarter.startActivityMayWait(null, callingUid, callingPackage, intent,
- resolvedType, null, null, null, null, 0, 0, null, null, null, bOptions, false,
- userId, null, "startAssistantActivity");
+
+ return mActivityStartController.obtainStarter(intent, "startAssistantActivity")
+ .setCallingUid(callingUid)
+ .setCallingPackage(callingPackage)
+ .setResolvedType(resolvedType)
+ .setMayWait(bOptions, userId)
+ .execute();
}
@Override
@@ -4771,9 +4935,12 @@ public class ActivityManagerService extends IActivityManager.Stub
intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
intent.setComponent(recentsComponent);
intent.putExtras(options);
- return mActivityStarter.startActivityMayWait(null, recentsUid, recentsPackage,
- intent, null, null, null, null, null, 0, 0, null, null, null, activityOptions,
- false, userId, null, "startRecentsActivity");
+
+ return mActivityStartController.obtainStarter(intent, "startRecentsActivity")
+ .setCallingUid(recentsUid)
+ .setCallingPackage(recentsPackage)
+ .setMayWait(activityOptions, userId)
+ .execute();
}
} finally {
Binder.restoreCallingIdentity(origId);
@@ -4946,11 +5113,22 @@ public class ActivityManagerService extends IActivityManager.Stub
}
final long origId = Binder.clearCallingIdentity();
- int res = mActivityStarter.startActivityLocked(r.app.thread, intent,
- null /*ephemeralIntent*/, r.resolvedType, aInfo, null /*rInfo*/, null,
- null, resultTo != null ? resultTo.appToken : null, resultWho, requestCode, -1,
- r.launchedFromUid, r.launchedFromPackage, -1, r.launchedFromUid, 0, options,
- false, false, null, null, "startNextMatchingActivity");
+ // TODO(b/64750076): Check if calling pid should really be -1.
+ final int res = mActivityStartController
+ .obtainStarter(intent, "startNextMatchingActivity")
+ .setCaller(r.app.thread)
+ .setResolvedType(r.resolvedType)
+ .setActivityInfo(aInfo)
+ .setResultTo(resultTo != null ? resultTo.appToken : null)
+ .setResultWho(resultWho)
+ .setRequestCode(requestCode)
+ .setCallingPid(-1)
+ .setCallingUid(r.launchedFromUid)
+ .setCallingPackage(r.launchedFromPackage)
+ .setRealCallingPid(-1)
+ .setRealCallingUid(r.launchedFromUid)
+ .setActivityOptions(options)
+ .execute();
Binder.restoreCallingIdentity(origId);
r.finishing = wasFinishing;
@@ -4976,20 +5154,6 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
- final int startActivityInPackage(int uid, String callingPackage,
- Intent intent, String resolvedType, IBinder resultTo,
- String resultWho, int requestCode, int startFlags, Bundle bOptions, int userId,
- TaskRecord inTask, String reason) {
-
- userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
- userId, false, ALLOW_FULL_ONLY, "startActivityInPackage", null);
-
- // TODO: Switch to user app stacks here.
- return mActivityStarter.startActivityMayWait(null, uid, callingPackage, intent,
- resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
- null, null, null, bOptions, false, userId, inTask, reason);
- }
-
@Override
public final int startActivities(IApplicationThread caller, String callingPackage,
Intent[] intents, String[] resolvedTypes, IBinder resultTo, Bundle bOptions,
@@ -4999,21 +5163,8 @@ public class ActivityManagerService extends IActivityManager.Stub
userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
userId, false, ALLOW_FULL_ONLY, reason, null);
// TODO: Switch to user app stacks here.
- int ret = mActivityStarter.startActivities(caller, -1, callingPackage, intents,
- resolvedTypes, resultTo, bOptions, userId, reason);
- return ret;
- }
-
- final int startActivitiesInPackage(int uid, String callingPackage,
- Intent[] intents, String[] resolvedTypes, IBinder resultTo,
- Bundle bOptions, int userId) {
-
- final String reason = "startActivityInPackage";
- userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
- userId, false, ALLOW_FULL_ONLY, reason, null);
- // TODO: Switch to user app stacks here.
- int ret = mActivityStarter.startActivities(null, uid, callingPackage, intents, resolvedTypes,
- resultTo, bOptions, userId, reason);
+ int ret = mActivityStartController.startActivities(caller, -1, callingPackage,
+ intents, resolvedTypes, resultTo, bOptions, userId, reason);
return ret;
}
@@ -5904,7 +6055,7 @@ public class ActivityManagerService extends IActivityManager.Stub
return;
}
- if (app != null) {
+ if (app != null && app.pid > 0) {
ArrayList<Integer> firstPids = new ArrayList<Integer>();
firstPids.add(app.pid);
dumpStackTraces(tracesPath, firstPids, null, null, true /* useTombstoned */);
@@ -5958,7 +6109,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public boolean clearApplicationUserData(final String packageName,
+ public boolean clearApplicationUserData(final String packageName, boolean keepState,
final IPackageDataObserver observer, int userId) {
enforceNotIsolatedCaller("clearApplicationUserData");
int uid = Binder.getCallingUid();
@@ -6062,14 +6213,27 @@ public class ActivityManagerService extends IActivityManager.Stub
pm.clearApplicationUserData(packageName, localObserver, resolvedUserId);
if (appInfo != null) {
- synchronized (this) {
- // Remove all permissions granted from/to this package
- removeUriPermissionsForPackageLocked(packageName, resolvedUserId, true);
+ // Restore already established notification state and permission grants,
+ // so it told us to keep those intact -- it's about to emplace app data
+ // that is appropriate for those bits of system state.
+ if (!keepState) {
+ synchronized (this) {
+ // Remove all permissions granted from/to this package
+ removeUriPermissionsForPackageLocked(packageName, resolvedUserId, true);
+ }
+
+ // Reset notification state
+ INotificationManager inm = NotificationManager.getService();
+ inm.clearData(packageName, appInfo.uid, uid == appInfo.uid);
}
- // Reset notification settings.
- INotificationManager inm = NotificationManager.getService();
- inm.clearData(packageName, appInfo.uid, uid == appInfo.uid);
+ // Clear its scheduled jobs
+ JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class);
+ js.cancelJobsForUid(appInfo.uid, "clear data");
+
+ // Clear its pending alarms
+ AlarmManagerInternal ami = LocalServices.getService(AlarmManagerInternal.class);
+ ami.removeAlarmsForUid(uid);
}
} catch (RemoteException e) {
}
@@ -6667,7 +6831,7 @@ public class ActivityManagerService extends IActivityManager.Stub
ProcessList.INVALID_ADJ, callerWillRestart, true, doit, evenPersistent,
packageName == null ? ("stop user " + userId) : ("stop " + packageName));
- didSomething |= mActivityStarter.clearPendingActivityLaunchesLocked(packageName);
+ didSomething |= mActivityStartController.clearPendingActivityLaunches(packageName);
if (mStackSupervisor.finishDisabledPackageActivitiesLocked(
packageName, null, doit, evenPersistent, userId)) {
@@ -6866,13 +7030,19 @@ public class ActivityManagerService extends IActivityManager.Stub
mHeavyWeightProcess = null;
}
boolean needRestart = false;
- if (app.pid > 0 && app.pid != MY_PID) {
+ if ((app.pid > 0 && app.pid != MY_PID) || (app.pid == 0 && app.pendingStart)) {
int pid = app.pid;
- synchronized (mPidsSelfLocked) {
- mPidsSelfLocked.remove(pid);
- mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
+ if (pid > 0) {
+ synchronized (mPidsSelfLocked) {
+ mPidsSelfLocked.remove(pid);
+ mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
+ }
+ mBatteryStatsService.noteProcessFinish(app.processName, app.info.uid);
+ if (app.isolated) {
+ mBatteryStatsService.removeIsolatedUid(app.uid, app.info.uid);
+ getPackageManagerInternalLocked().removeIsolatedUid(app.uid);
+ }
}
- mBatteryStatsService.noteProcessFinish(app.processName, app.info.uid);
boolean willRestart = false;
if (app.persistent && !app.isolated) {
if (!callerWillRestart) {
@@ -6882,10 +7052,6 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
app.kill(reason, true);
- if (app.isolated) {
- mBatteryStatsService.removeIsolatedUid(app.uid, app.info.uid);
- getPackageManagerInternalLocked().removeIsolatedUid(app.uid);
- }
handleAppDiedLocked(app, willRestart, allowRestart);
if (willRestart) {
removeLruProcessLocked(app);
@@ -6959,7 +7125,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
private final boolean attachApplicationLocked(IApplicationThread thread,
- int pid) {
+ int pid, int callingUid, long startSeq) {
// Find the application record that is being attached... either via
// the pid if we are running in multiple processes, or just pull the
@@ -6974,6 +7140,17 @@ public class ActivityManagerService extends IActivityManager.Stub
app = null;
}
+ // It's possible that process called attachApplication before we got a chance to
+ // update the internal state.
+ if (app == null && startSeq > 0) {
+ final ProcessRecord pending = mPendingStarts.get(startSeq);
+ if (pending != null && pending.startUid == callingUid
+ && handleProcessStartedLocked(pending, pid, pending.usingWrapper,
+ startSeq, true)) {
+ app = pending;
+ }
+ }
+
if (app == null) {
Slog.w(TAG, "No pending application record for pid " + pid
+ " (IApplicationThread " + thread + "); dropping process");
@@ -7268,11 +7445,12 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public final void attachApplication(IApplicationThread thread) {
+ public final void attachApplication(IApplicationThread thread, long startSeq) {
synchronized (this) {
int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
- attachApplicationLocked(thread, callingPid);
+ attachApplicationLocked(thread, callingPid, callingUid, startSeq);
Binder.restoreCallingIdentity(origId);
}
}
@@ -8887,7 +9065,10 @@ public class ActivityManagerService extends IActivityManager.Stub
final int callingAppId = UserHandle.getAppId(callingUid);
if ((callingAppId == SYSTEM_UID) || (callingAppId == ROOT_UID)) {
if ("com.android.settings.files".equals(grantUri.uri.getAuthority())) {
- // Exempted authority for cropping user photos in Settings app
+ // Exempted authority for
+ // 1. cropping user photos and sharing a generated license html
+ // file in Settings app
+ // 2. sharing a generated license html file in TvSettings app
} else {
Slog.w(TAG, "For security reasons, the system cannot issue a Uri permission"
+ " grant to " + grantUri + "; use startActivityAsCaller() instead");
@@ -10031,7 +10212,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
- TaskRecord task = new TaskRecord(this,
+ TaskRecord task = TaskRecord.create(this,
mStackSupervisor.getNextTaskIdForUserLocked(r.userId),
ainfo, intent, description);
if (!mRecentTasks.addToBottom(task)) {
@@ -10183,26 +10364,6 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public void cancelTaskThumbnailTransition(int taskId) {
- enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS,
- "cancelTaskThumbnailTransition()");
- final long ident = Binder.clearCallingIdentity();
- try {
- synchronized (this) {
- final TaskRecord task = mStackSupervisor.anyTaskForIdLocked(taskId,
- MATCH_TASK_IN_STACKS_ONLY);
- if (task == null) {
- Slog.w(TAG, "cancelTaskThumbnailTransition: taskId=" + taskId + " not found");
- return;
- }
- task.cancelThumbnailTransition();
- }
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
-
- @Override
public TaskSnapshot getTaskSnapshot(int taskId, boolean reducedResolution) {
enforceCallerIsRecentsOrHasPermission(READ_FRAME_BUFFER, "getTaskSnapshot()");
final long ident = Binder.clearCallingIdentity();
@@ -10501,7 +10662,7 @@ public class ActivityManagerService extends IActivityManager.Stub
public void setTaskWindowingMode(int taskId, int windowingMode, boolean toTop) {
if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
setTaskWindowingModeSplitScreenPrimary(taskId, SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT,
- toTop, ANIMATE, null /* initialBounds */);
+ toTop, ANIMATE, null /* initialBounds */, true /* showRecents */);
return;
}
enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS, "setTaskWindowingMode()");
@@ -10547,10 +10708,12 @@ public class ActivityManagerService extends IActivityManager.Stub
* @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
* 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.
*/
@Override
public boolean setTaskWindowingModeSplitScreenPrimary(int taskId, int createMode, boolean toTop,
- boolean animate, Rect initialBounds) {
+ boolean animate, Rect initialBounds, boolean showRecents) {
enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS,
"setTaskWindowingModeSplitScreenPrimary()");
synchronized (this) {
@@ -10575,7 +10738,8 @@ public class ActivityManagerService extends IActivityManager.Stub
if (toTop) {
stack.moveToFront("setTaskWindowingModeSplitScreenPrimary", task);
}
- stack.setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, animate);
+ stack.setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, animate, showRecents,
+ false /* enteringSplitScreenMode */);
return windowingMode != task.getWindowingMode();
} finally {
Binder.restoreCallingIdentity(ident);
@@ -11330,7 +11494,11 @@ public class ActivityManagerService extends IActivityManager.Stub
private final long[] mProcessStateStatsLongs = new long[1];
- boolean isProcessAliveLocked(ProcessRecord proc) {
+ private boolean isProcessAliveLocked(ProcessRecord proc) {
+ if (proc.pid <= 0) {
+ if (DEBUG_OOM_ADJ) Slog.d(TAG, "Process hasn't started yet: " + proc);
+ return false;
+ }
if (proc.procStatFile == null) {
proc.procStatFile = "/proc/" + proc.pid + "/stat";
}
@@ -11755,6 +11923,10 @@ public class ActivityManagerService extends IActivityManager.Stub
return AppGlobals.getPackageManager();
}
+ ActivityStartController getActivityStartController() {
+ return mActivityStartController;
+ }
+
PackageManagerInternal getPackageManagerInternalLocked() {
if (mPackageManagerInt == null) {
mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class);
@@ -12412,7 +12584,8 @@ public class ActivityManagerService extends IActivityManager.Stub
if (wasAwake != isAwake) {
// Also update state in a special way for running foreground services UI.
mServices.updateScreenStateLocked(isAwake);
- sendNotifyVrManagerOfSleepState(!isAwake);
+ mHandler.obtainMessage(DISPATCH_SCREEN_AWAKE_MSG, isAwake ? 1 : 0, 0)
+ .sendToTarget();
}
}
}
@@ -12568,7 +12741,9 @@ public class ActivityManagerService extends IActivityManager.Stub
Binder.restoreCallingIdentity(ident);
}
}
- sendNotifyVrManagerOfKeyguardState(showing);
+
+ mHandler.obtainMessage(DISPATCH_SCREEN_KEYGUARD_MSG, showing ? 1 : 0, 0)
+ .sendToTarget();
}
@Override
@@ -12604,7 +12779,16 @@ public class ActivityManagerService extends IActivityManager.Stub
synchronized (this) {
final long ident = Binder.clearCallingIdentity();
try {
- mActivityStarter.startConfirmCredentialIntent(intent, options);
+ intent.addFlags(FLAG_ACTIVITY_NEW_TASK |
+ FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS |
+ FLAG_ACTIVITY_TASK_ON_HOME);
+ ActivityOptions activityOptions = options != null
+ ? new ActivityOptions(options)
+ : ActivityOptions.makeBasic();
+ activityOptions.setLaunchTaskId(
+ mStackSupervisor.getHomeActivity().getTask().taskId);
+ mContext.startActivityAsUser(intent, activityOptions.toBundle(),
+ UserHandle.CURRENT);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -12623,9 +12807,7 @@ public class ActivityManagerService extends IActivityManager.Stub
mAppSwitchesAllowedTime = SystemClock.uptimeMillis()
+ APP_SWITCH_DELAY_TIME;
mDidAppSwitch = false;
- mHandler.removeMessages(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
- Message msg = mHandler.obtainMessage(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
- mHandler.sendMessageDelayed(msg, APP_SWITCH_DELAY_TIME);
+ mActivityStartController.schedulePendingActivityLaunches(APP_SWITCH_DELAY_TIME);
}
}
@@ -13696,7 +13878,8 @@ public class ActivityManagerService extends IActivityManager.Stub
stats.getPackageStatsLocked(sourceUid >= 0 ? sourceUid : uid,
sourcePkg != null ? sourcePkg : rec.key.packageName);
pkg.noteWakeupAlarmLocked(tag);
- StatsLog.write(StatsLog.WAKEUP_ALARM_OCCURRED, sourceUid >= 0 ? sourceUid : uid);
+ StatsLog.write(StatsLog.WAKEUP_ALARM_OCCURRED, sourceUid >= 0 ? sourceUid : uid,
+ tag);
}
}
}
@@ -13943,8 +14126,7 @@ public class ActivityManagerService extends IActivityManager.Stub
for (int i = mLruProcesses.size() - 1 ; i >= 0 ; i--) {
ProcessRecord proc = mLruProcesses.get(i);
if (proc.notCachedSinceIdle) {
- if (proc.setProcState != ActivityManager.PROCESS_STATE_TOP_SLEEPING
- && proc.setProcState >= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+ if (proc.setProcState >= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
&& proc.setProcState <= ActivityManager.PROCESS_STATE_SERVICE) {
if (doKilling && proc.initialIdlePss != 0
&& proc.lastPss > ((proc.initialIdlePss*3)/2)) {
@@ -14595,7 +14777,7 @@ public class ActivityManagerService extends IActivityManager.Stub
try {
PackageInfo pi = pm.getPackageInfo(pkg, 0, UserHandle.getCallingUserId());
if (pi != null) {
- sb.append(" v").append(pi.versionCode);
+ sb.append(" v").append(pi.getLongVersionCode());
if (pi.versionName != null) {
sb.append(" (").append(pi.versionName).append(")");
}
@@ -15490,7 +15672,7 @@ public class ActivityManagerService extends IActivityManager.Stub
private void dumpActivityStarterLocked(PrintWriter pw, String dumpPackage) {
pw.println("ACTIVITY MANAGER STARTER (dumpsys activity starter)");
- mActivityStarter.dump(pw, "", dumpPackage);
+ mActivityStartController.dump(pw, "", dumpPackage);
}
void dumpActivitiesLocked(FileDescriptor fd, PrintWriter pw, String[] args,
@@ -15876,6 +16058,14 @@ public class ActivityManagerService extends IActivityManager.Stub
}
pw.println(" mHeavyWeightProcess: " + mHeavyWeightProcess);
}
+ if (dumpAll && mPendingStarts.size() > 0) {
+ if (needSep) pw.println();
+ needSep = true;
+ pw.println(" mPendingStarts: ");
+ for (int i = 0, len = mPendingStarts.size(); i < len; ++i ) {
+ pw.println(" " + mPendingStarts.keyAt(i) + ": " + mPendingStarts.valueAt(i));
+ }
+ }
if (dumpPackage == null) {
pw.println(" mGlobalConfiguration: " + getGlobalConfiguration());
mStackSupervisor.dumpDisplayConfigs(pw, " ");
@@ -16910,7 +17100,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
for (int i=mLruProcesses.size()-1; i>=0; i--) {
ProcessRecord proc = mLruProcesses.get(i);
- if (proc.pid == pid) {
+ if (proc.pid > 0 && proc.pid == pid) {
procs.add(proc);
} else if (allPkgs && proc.pkgList != null
&& proc.pkgList.containsKey(args[start])) {
@@ -17031,20 +17221,24 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
+ private static void sortMemItems(List<MemItem> items) {
+ Collections.sort(items, new Comparator<MemItem>() {
+ @Override
+ public int compare(MemItem lhs, MemItem rhs) {
+ if (lhs.pss < rhs.pss) {
+ return 1;
+ } else if (lhs.pss > rhs.pss) {
+ return -1;
+ }
+ return 0;
+ }
+ });
+ }
+
static final void dumpMemItems(PrintWriter pw, String prefix, String tag,
ArrayList<MemItem> items, boolean sort, boolean isCompact, boolean dumpSwapPss) {
if (sort && !isCompact) {
- Collections.sort(items, new Comparator<MemItem>() {
- @Override
- public int compare(MemItem lhs, MemItem rhs) {
- if (lhs.pss < rhs.pss) {
- return 1;
- } else if (lhs.pss > rhs.pss) {
- return -1;
- }
- return 0;
- }
- });
+ sortMemItems(items);
}
for (int i=0; i<items.size(); i++) {
@@ -17072,6 +17266,33 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
+ static final void dumpMemItems(ProtoOutputStream proto, long fieldId, String tag,
+ ArrayList<MemItem> items, boolean sort, boolean dumpSwapPss) {
+ if (sort) {
+ sortMemItems(items);
+ }
+
+ for (int i=0; i<items.size(); i++) {
+ MemItem mi = items.get(i);
+ final long token = proto.start(fieldId);
+
+ proto.write(MemInfoProto.MemItem.TAG, tag);
+ proto.write(MemInfoProto.MemItem.LABEL, mi.shortLabel);
+ proto.write(MemInfoProto.MemItem.IS_PROC, mi.isProc);
+ proto.write(MemInfoProto.MemItem.ID, mi.id);
+ proto.write(MemInfoProto.MemItem.HAS_ACTIVITIES, mi.hasActivities);
+ proto.write(MemInfoProto.MemItem.PSS_KB, mi.pss);
+ if (dumpSwapPss) {
+ proto.write(MemInfoProto.MemItem.SWAP_PSS_KB, mi.swapPss);
+ }
+ if (mi.subitems != null) {
+ dumpMemItems(proto, MemInfoProto.MemItem.SUB_ITEMS, mi.shortLabel, mi.subitems,
+ true, dumpSwapPss);
+ }
+ proto.end(token);
+ }
+ }
+
// These are in KB.
static final long[] DUMP_MEM_BUCKETS = new long[] {
5*1024, 7*1024, 10*1024, 15*1024, 20*1024, 30*1024, 40*1024, 80*1024,
@@ -17207,8 +17428,8 @@ public class ActivityManagerService extends IActivityManager.Stub
boolean dumpProto;
}
- final void dumpApplicationMemoryUsage(FileDescriptor fd,
- PrintWriter pw, String prefix, String[] args, boolean brief, PrintWriter categoryPw) {
+ final void dumpApplicationMemoryUsage(FileDescriptor fd, PrintWriter pw, String prefix,
+ String[] args, boolean brief, PrintWriter categoryPw, boolean asProto) {
MemoryUsageDumpOptions opts = new MemoryUsageDumpOptions();
opts.dumpDetails = false;
opts.dumpFullDetails = false;
@@ -17221,7 +17442,7 @@ public class ActivityManagerService extends IActivityManager.Stub
opts.packages = false;
opts.isCheckinRequest = false;
opts.dumpSwapPss = false;
- opts.dumpProto = false;
+ opts.dumpProto = asProto;
int opti = 0;
while (opti < args.length) {
@@ -17283,13 +17504,13 @@ public class ActivityManagerService extends IActivityManager.Stub
ArrayList<ProcessRecord> procs = collectProcesses(pw, opti, opts.packages, args);
if (opts.dumpProto) {
- dumpApplicationMemoryUsage(fd, pw, opts, innerArgs, brief, procs);
+ dumpApplicationMemoryUsage(fd, opts, innerArgs, brief, procs);
} else {
dumpApplicationMemoryUsage(fd, pw, prefix, opts, innerArgs, brief, procs, categoryPw);
}
}
- final void dumpApplicationMemoryUsage(FileDescriptor fd, PrintWriter pw, String prefix,
+ private final void dumpApplicationMemoryUsage(FileDescriptor fd, PrintWriter pw, String prefix,
MemoryUsageDumpOptions opts, String[] innerArgs, boolean brief,
ArrayList<ProcessRecord> procs, PrintWriter categoryPw) {
long uptime = SystemClock.uptimeMillis();
@@ -17298,54 +17519,59 @@ public class ActivityManagerService extends IActivityManager.Stub
if (procs == null) {
// No Java processes. Maybe they want to print a native process.
- if (innerArgs.length > 0 && innerArgs[0].charAt(0) != '-') {
- ArrayList<ProcessCpuTracker.Stats> nativeProcs
- = new ArrayList<ProcessCpuTracker.Stats>();
- updateCpuStatsNow();
- int findPid = -1;
- try {
- findPid = Integer.parseInt(innerArgs[0]);
- } catch (NumberFormatException e) {
- }
- synchronized (mProcessCpuTracker) {
- final int N = mProcessCpuTracker.countStats();
- for (int i=0; i<N; i++) {
- ProcessCpuTracker.Stats st = mProcessCpuTracker.getStats(i);
- if (st.pid == findPid || (st.baseName != null
- && st.baseName.equals(innerArgs[0]))) {
- nativeProcs.add(st);
- }
+ String proc = "N/A";
+ if (innerArgs.length > 0) {
+ proc = innerArgs[0];
+ if (proc.charAt(0) != '-') {
+ ArrayList<ProcessCpuTracker.Stats> nativeProcs
+ = new ArrayList<ProcessCpuTracker.Stats>();
+ updateCpuStatsNow();
+ int findPid = -1;
+ try {
+ findPid = Integer.parseInt(innerArgs[0]);
+ } catch (NumberFormatException e) {
}
- }
- if (nativeProcs.size() > 0) {
- dumpApplicationMemoryUsageHeader(pw, uptime, realtime, opts.isCheckinRequest,
- opts.isCompact);
- Debug.MemoryInfo mi = null;
- for (int i = nativeProcs.size() - 1 ; i >= 0 ; i--) {
- final ProcessCpuTracker.Stats r = nativeProcs.get(i);
- final int pid = r.pid;
- if (!opts.isCheckinRequest && opts.dumpDetails) {
- pw.println("\n** MEMINFO in pid " + pid + " [" + r.baseName + "] **");
- }
- if (mi == null) {
- mi = new Debug.MemoryInfo();
- }
- if (opts.dumpDetails || (!brief && !opts.oomOnly)) {
- Debug.getMemoryInfo(pid, mi);
- } else {
- mi.dalvikPss = (int)Debug.getPss(pid, tmpLong, null);
- mi.dalvikPrivateDirty = (int)tmpLong[0];
+ synchronized (mProcessCpuTracker) {
+ final int N = mProcessCpuTracker.countStats();
+ for (int i=0; i<N; i++) {
+ ProcessCpuTracker.Stats st = mProcessCpuTracker.getStats(i);
+ if (st.pid == findPid || (st.baseName != null
+ && st.baseName.equals(innerArgs[0]))) {
+ nativeProcs.add(st);
+ }
}
- ActivityThread.dumpMemInfoTable(pw, mi, opts.isCheckinRequest, opts.dumpFullDetails,
- opts.dumpDalvik, opts.dumpSummaryOnly, pid, r.baseName, 0, 0, 0, 0, 0, 0);
- if (opts.isCheckinRequest) {
- pw.println();
+ }
+ if (nativeProcs.size() > 0) {
+ dumpApplicationMemoryUsageHeader(pw, uptime, realtime,
+ opts.isCheckinRequest, opts.isCompact);
+ Debug.MemoryInfo mi = null;
+ for (int i = nativeProcs.size() - 1 ; i >= 0 ; i--) {
+ final ProcessCpuTracker.Stats r = nativeProcs.get(i);
+ final int pid = r.pid;
+ if (!opts.isCheckinRequest && opts.dumpDetails) {
+ pw.println("\n** MEMINFO in pid " + pid + " [" + r.baseName + "] **");
+ }
+ if (mi == null) {
+ mi = new Debug.MemoryInfo();
+ }
+ if (opts.dumpDetails || (!brief && !opts.oomOnly)) {
+ Debug.getMemoryInfo(pid, mi);
+ } else {
+ mi.dalvikPss = (int)Debug.getPss(pid, tmpLong, null);
+ mi.dalvikPrivateDirty = (int)tmpLong[0];
+ }
+ ActivityThread.dumpMemInfoTable(pw, mi, opts.isCheckinRequest,
+ opts.dumpFullDetails, opts.dumpDalvik, opts.dumpSummaryOnly,
+ pid, r.baseName, 0, 0, 0, 0, 0, 0);
+ if (opts.isCheckinRequest) {
+ pw.println();
+ }
}
+ return;
}
- return;
}
}
- pw.println("No process found for: " + innerArgs[0]);
+ pw.println("No process found for: " + proc);
return;
}
@@ -17768,13 +17994,399 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
- final void dumpApplicationMemoryUsage(FileDescriptor fd, PrintWriter pw,
+ private final void dumpApplicationMemoryUsage(FileDescriptor fd,
MemoryUsageDumpOptions opts, String[] innerArgs, boolean brief,
ArrayList<ProcessRecord> procs) {
+ final long uptimeMs = SystemClock.uptimeMillis();
+ final long realtimeMs = SystemClock.elapsedRealtime();
+ final long[] tmpLong = new long[1];
+
+ if (procs == null) {
+ // No Java processes. Maybe they want to print a native process.
+ String proc = "N/A";
+ if (innerArgs.length > 0) {
+ proc = innerArgs[0];
+ if (proc.charAt(0) != '-') {
+ ArrayList<ProcessCpuTracker.Stats> nativeProcs
+ = new ArrayList<ProcessCpuTracker.Stats>();
+ updateCpuStatsNow();
+ int findPid = -1;
+ try {
+ findPid = Integer.parseInt(innerArgs[0]);
+ } catch (NumberFormatException e) {
+ }
+ synchronized (mProcessCpuTracker) {
+ final int N = mProcessCpuTracker.countStats();
+ for (int i=0; i<N; i++) {
+ ProcessCpuTracker.Stats st = mProcessCpuTracker.getStats(i);
+ if (st.pid == findPid || (st.baseName != null
+ && st.baseName.equals(innerArgs[0]))) {
+ nativeProcs.add(st);
+ }
+ }
+ }
+ if (nativeProcs.size() > 0) {
+ ProtoOutputStream proto = new ProtoOutputStream(fd);
+
+ proto.write(MemInfoProto.UPTIME_DURATION_MS, uptimeMs);
+ proto.write(MemInfoProto.ELAPSED_REALTIME_MS, realtimeMs);
+ Debug.MemoryInfo mi = null;
+ for (int i = nativeProcs.size() - 1 ; i >= 0 ; i--) {
+ final ProcessCpuTracker.Stats r = nativeProcs.get(i);
+ final int pid = r.pid;
+ final long nToken = proto.start(MemInfoProto.NATIVE_PROCESSES);
+
+ proto.write(MemInfoProto.ProcessMemory.PID, pid);
+ proto.write(MemInfoProto.ProcessMemory.PROCESS_NAME, r.baseName);
+
+ if (mi == null) {
+ mi = new Debug.MemoryInfo();
+ }
+ if (opts.dumpDetails || (!brief && !opts.oomOnly)) {
+ Debug.getMemoryInfo(pid, mi);
+ } else {
+ mi.dalvikPss = (int)Debug.getPss(pid, tmpLong, null);
+ mi.dalvikPrivateDirty = (int)tmpLong[0];
+ }
+ ActivityThread.dumpMemInfoTable(proto, mi, opts.dumpDalvik,
+ opts.dumpSummaryOnly, 0, 0, 0, 0, 0, 0);
+
+ proto.end(nToken);
+ }
+
+ proto.flush();
+ return;
+ }
+ }
+ }
+ Log.d(TAG, "No process found for: " + innerArgs[0]);
+ return;
+ }
+
+ if (!brief && !opts.oomOnly && (procs.size() == 1 || opts.isCheckinRequest || opts.packages)) {
+ opts.dumpDetails = true;
+ }
+
ProtoOutputStream proto = new ProtoOutputStream(fd);
- // TODO: implement
- pw.println("Not yet implemented. Have a cookie instead! :]");
+ proto.write(MemInfoProto.UPTIME_DURATION_MS, uptimeMs);
+ proto.write(MemInfoProto.ELAPSED_REALTIME_MS, realtimeMs);
+
+ ArrayList<MemItem> procMems = new ArrayList<MemItem>();
+ final SparseArray<MemItem> procMemsMap = new SparseArray<MemItem>();
+ long nativePss = 0;
+ long nativeSwapPss = 0;
+ long dalvikPss = 0;
+ long dalvikSwapPss = 0;
+ long[] dalvikSubitemPss = opts.dumpDalvik ? new long[Debug.MemoryInfo.NUM_DVK_STATS] :
+ EmptyArray.LONG;
+ long[] dalvikSubitemSwapPss = opts.dumpDalvik ? new long[Debug.MemoryInfo.NUM_DVK_STATS] :
+ EmptyArray.LONG;
+ long otherPss = 0;
+ long otherSwapPss = 0;
+ long[] miscPss = new long[Debug.MemoryInfo.NUM_OTHER_STATS];
+ long[] miscSwapPss = new long[Debug.MemoryInfo.NUM_OTHER_STATS];
+
+ long oomPss[] = new long[DUMP_MEM_OOM_LABEL.length];
+ long oomSwapPss[] = new long[DUMP_MEM_OOM_LABEL.length];
+ ArrayList<MemItem>[] oomProcs = (ArrayList<MemItem>[])
+ new ArrayList[DUMP_MEM_OOM_LABEL.length];
+
+ long totalPss = 0;
+ long totalSwapPss = 0;
+ long cachedPss = 0;
+ long cachedSwapPss = 0;
+ boolean hasSwapPss = false;
+
+ Debug.MemoryInfo mi = null;
+ for (int i = procs.size() - 1 ; i >= 0 ; i--) {
+ final ProcessRecord r = procs.get(i);
+ final IApplicationThread thread;
+ final int pid;
+ final int oomAdj;
+ final boolean hasActivities;
+ synchronized (this) {
+ thread = r.thread;
+ pid = r.pid;
+ oomAdj = r.getSetAdjWithServices();
+ hasActivities = r.activities.size() > 0;
+ }
+ if (thread == null) {
+ continue;
+ }
+ if (mi == null) {
+ mi = new Debug.MemoryInfo();
+ }
+ if (opts.dumpDetails || (!brief && !opts.oomOnly)) {
+ Debug.getMemoryInfo(pid, mi);
+ hasSwapPss = mi.hasSwappedOutPss;
+ } else {
+ mi.dalvikPss = (int) Debug.getPss(pid, tmpLong, null);
+ mi.dalvikPrivateDirty = (int) tmpLong[0];
+ }
+ if (opts.dumpDetails) {
+ if (opts.localOnly) {
+ final long aToken = proto.start(MemInfoProto.APP_PROCESSES);
+ final long mToken = proto.start(MemInfoProto.AppData.PROCESS_MEMORY);
+ proto.write(MemInfoProto.ProcessMemory.PID, pid);
+ proto.write(MemInfoProto.ProcessMemory.PROCESS_NAME, r.processName);
+ ActivityThread.dumpMemInfoTable(proto, mi, opts.dumpDalvik,
+ opts.dumpSummaryOnly, 0, 0, 0, 0, 0, 0);
+ proto.end(mToken);
+ proto.end(aToken);
+ } else {
+ try {
+ ByteTransferPipe tp = new ByteTransferPipe();
+ try {
+ thread.dumpMemInfoProto(tp.getWriteFd(),
+ mi, opts.dumpFullDetails, opts.dumpDalvik, opts.dumpSummaryOnly,
+ opts.dumpUnreachable, innerArgs);
+ proto.write(MemInfoProto.APP_PROCESSES, tp.get());
+ } finally {
+ tp.kill();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Got IOException!", e);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Got RemoteException!", e);
+ }
+ }
+ }
+
+ final long myTotalPss = mi.getTotalPss();
+ final long myTotalUss = mi.getTotalUss();
+ final long myTotalSwapPss = mi.getTotalSwappedOutPss();
+
+ synchronized (this) {
+ if (r.thread != null && oomAdj == r.getSetAdjWithServices()) {
+ // Record this for posterity if the process has been stable.
+ r.baseProcessTracker.addPss(myTotalPss, myTotalUss, true, r.pkgList);
+ }
+ }
+
+ if (!opts.isCheckinRequest && mi != null) {
+ totalPss += myTotalPss;
+ totalSwapPss += myTotalSwapPss;
+ MemItem pssItem = new MemItem(r.processName + " (pid " + pid +
+ (hasActivities ? " / activities)" : ")"), r.processName, myTotalPss,
+ myTotalSwapPss, pid, hasActivities);
+ procMems.add(pssItem);
+ procMemsMap.put(pid, pssItem);
+
+ nativePss += mi.nativePss;
+ nativeSwapPss += mi.nativeSwappedOutPss;
+ dalvikPss += mi.dalvikPss;
+ dalvikSwapPss += mi.dalvikSwappedOutPss;
+ for (int j=0; j<dalvikSubitemPss.length; j++) {
+ dalvikSubitemPss[j] += mi.getOtherPss(Debug.MemoryInfo.NUM_OTHER_STATS + j);
+ dalvikSubitemSwapPss[j] +=
+ mi.getOtherSwappedOutPss(Debug.MemoryInfo.NUM_OTHER_STATS + j);
+ }
+ otherPss += mi.otherPss;
+ otherSwapPss += mi.otherSwappedOutPss;
+ for (int j=0; j<Debug.MemoryInfo.NUM_OTHER_STATS; j++) {
+ long mem = mi.getOtherPss(j);
+ miscPss[j] += mem;
+ otherPss -= mem;
+ mem = mi.getOtherSwappedOutPss(j);
+ miscSwapPss[j] += mem;
+ otherSwapPss -= mem;
+ }
+
+ if (oomAdj >= ProcessList.CACHED_APP_MIN_ADJ) {
+ cachedPss += myTotalPss;
+ cachedSwapPss += myTotalSwapPss;
+ }
+
+ for (int oomIndex=0; oomIndex<oomPss.length; oomIndex++) {
+ if (oomIndex == (oomPss.length - 1)
+ || (oomAdj >= DUMP_MEM_OOM_ADJ[oomIndex]
+ && oomAdj < DUMP_MEM_OOM_ADJ[oomIndex + 1])) {
+ oomPss[oomIndex] += myTotalPss;
+ oomSwapPss[oomIndex] += myTotalSwapPss;
+ if (oomProcs[oomIndex] == null) {
+ oomProcs[oomIndex] = new ArrayList<MemItem>();
+ }
+ oomProcs[oomIndex].add(pssItem);
+ break;
+ }
+ }
+ }
+ }
+
+ long nativeProcTotalPss = 0;
+
+ if (procs.size() > 1 && !opts.packages) {
+ // If we are showing aggregations, also look for native processes to
+ // include so that our aggregations are more accurate.
+ updateCpuStatsNow();
+ mi = null;
+ synchronized (mProcessCpuTracker) {
+ final int N = mProcessCpuTracker.countStats();
+ for (int i=0; i<N; i++) {
+ ProcessCpuTracker.Stats st = mProcessCpuTracker.getStats(i);
+ if (st.vsize > 0 && procMemsMap.indexOfKey(st.pid) < 0) {
+ if (mi == null) {
+ mi = new Debug.MemoryInfo();
+ }
+ if (!brief && !opts.oomOnly) {
+ Debug.getMemoryInfo(st.pid, mi);
+ } else {
+ mi.nativePss = (int)Debug.getPss(st.pid, tmpLong, null);
+ mi.nativePrivateDirty = (int)tmpLong[0];
+ }
+
+ final long myTotalPss = mi.getTotalPss();
+ final long myTotalSwapPss = mi.getTotalSwappedOutPss();
+ totalPss += myTotalPss;
+ nativeProcTotalPss += myTotalPss;
+
+ MemItem pssItem = new MemItem(st.name + " (pid " + st.pid + ")",
+ st.name, myTotalPss, mi.getSummaryTotalSwapPss(), st.pid, false);
+ procMems.add(pssItem);
+
+ nativePss += mi.nativePss;
+ nativeSwapPss += mi.nativeSwappedOutPss;
+ dalvikPss += mi.dalvikPss;
+ dalvikSwapPss += mi.dalvikSwappedOutPss;
+ for (int j=0; j<dalvikSubitemPss.length; j++) {
+ dalvikSubitemPss[j] += mi.getOtherPss(Debug.MemoryInfo.NUM_OTHER_STATS + j);
+ dalvikSubitemSwapPss[j] +=
+ mi.getOtherSwappedOutPss(Debug.MemoryInfo.NUM_OTHER_STATS + j);
+ }
+ otherPss += mi.otherPss;
+ otherSwapPss += mi.otherSwappedOutPss;
+ for (int j=0; j<Debug.MemoryInfo.NUM_OTHER_STATS; j++) {
+ long mem = mi.getOtherPss(j);
+ miscPss[j] += mem;
+ otherPss -= mem;
+ mem = mi.getOtherSwappedOutPss(j);
+ miscSwapPss[j] += mem;
+ otherSwapPss -= mem;
+ }
+ oomPss[0] += myTotalPss;
+ oomSwapPss[0] += myTotalSwapPss;
+ if (oomProcs[0] == null) {
+ oomProcs[0] = new ArrayList<MemItem>();
+ }
+ oomProcs[0].add(pssItem);
+ }
+ }
+ }
+
+ ArrayList<MemItem> catMems = new ArrayList<MemItem>();
+
+ catMems.add(new MemItem("Native", "Native", nativePss, nativeSwapPss, -1));
+ final int dalvikId = -2;
+ catMems.add(new MemItem("Dalvik", "Dalvik", dalvikPss, dalvikSwapPss, dalvikId));
+ catMems.add(new MemItem("Unknown", "Unknown", otherPss, otherSwapPss, -3));
+ for (int j=0; j<Debug.MemoryInfo.NUM_OTHER_STATS; j++) {
+ String label = Debug.MemoryInfo.getOtherLabel(j);
+ catMems.add(new MemItem(label, label, miscPss[j], miscSwapPss[j], j));
+ }
+ if (dalvikSubitemPss.length > 0) {
+ // Add dalvik subitems.
+ for (MemItem memItem : catMems) {
+ int memItemStart = 0, memItemEnd = 0;
+ if (memItem.id == dalvikId) {
+ memItemStart = Debug.MemoryInfo.OTHER_DVK_STAT_DALVIK_START;
+ memItemEnd = Debug.MemoryInfo.OTHER_DVK_STAT_DALVIK_END;
+ } else if (memItem.id == Debug.MemoryInfo.OTHER_DALVIK_OTHER) {
+ memItemStart = Debug.MemoryInfo.OTHER_DVK_STAT_DALVIK_OTHER_START;
+ memItemEnd = Debug.MemoryInfo.OTHER_DVK_STAT_DALVIK_OTHER_END;
+ } else if (memItem.id == Debug.MemoryInfo.OTHER_DEX) {
+ memItemStart = Debug.MemoryInfo.OTHER_DVK_STAT_DEX_START;
+ memItemEnd = Debug.MemoryInfo.OTHER_DVK_STAT_DEX_END;
+ } else if (memItem.id == Debug.MemoryInfo.OTHER_ART) {
+ memItemStart = Debug.MemoryInfo.OTHER_DVK_STAT_ART_START;
+ memItemEnd = Debug.MemoryInfo.OTHER_DVK_STAT_ART_END;
+ } else {
+ continue; // No subitems, continue.
+ }
+ memItem.subitems = new ArrayList<MemItem>();
+ for (int j=memItemStart; j<=memItemEnd; j++) {
+ final String name = Debug.MemoryInfo.getOtherLabel(
+ Debug.MemoryInfo.NUM_OTHER_STATS + j);
+ memItem.subitems.add(new MemItem(name, name, dalvikSubitemPss[j],
+ dalvikSubitemSwapPss[j], j));
+ }
+ }
+ }
+
+ ArrayList<MemItem> oomMems = new ArrayList<MemItem>();
+ for (int j=0; j<oomPss.length; j++) {
+ if (oomPss[j] != 0) {
+ String label = opts.isCompact ? DUMP_MEM_OOM_COMPACT_LABEL[j]
+ : DUMP_MEM_OOM_LABEL[j];
+ MemItem item = new MemItem(label, label, oomPss[j], oomSwapPss[j],
+ DUMP_MEM_OOM_ADJ[j]);
+ item.subitems = oomProcs[j];
+ oomMems.add(item);
+ }
+ }
+
+ opts.dumpSwapPss = opts.dumpSwapPss && hasSwapPss && totalSwapPss != 0;
+ if (!opts.oomOnly) {
+ dumpMemItems(proto, MemInfoProto.TOTAL_PSS_BY_PROCESS, "proc",
+ procMems, true, opts.dumpSwapPss);
+ }
+ dumpMemItems(proto, MemInfoProto.TOTAL_PSS_BY_OOM_ADJUSTMENT, "oom",
+ oomMems, false, opts.dumpSwapPss);
+ if (!brief && !opts.oomOnly) {
+ dumpMemItems(proto, MemInfoProto.TOTAL_PSS_BY_CATEGORY, "cat",
+ catMems, true, opts.dumpSwapPss);
+ }
+ MemInfoReader memInfo = new MemInfoReader();
+ memInfo.readMemInfo();
+ if (nativeProcTotalPss > 0) {
+ synchronized (this) {
+ final long cachedKb = memInfo.getCachedSizeKb();
+ final long freeKb = memInfo.getFreeSizeKb();
+ final long zramKb = memInfo.getZramTotalSizeKb();
+ final long kernelKb = memInfo.getKernelUsedSizeKb();
+ EventLogTags.writeAmMeminfo(cachedKb*1024, freeKb*1024, zramKb*1024,
+ kernelKb*1024, nativeProcTotalPss*1024);
+ mProcessStats.addSysMemUsageLocked(cachedKb, freeKb, zramKb, kernelKb,
+ nativeProcTotalPss);
+ }
+ }
+ if (!brief) {
+ proto.write(MemInfoProto.TOTAL_RAM_KB, memInfo.getTotalSizeKb());
+ proto.write(MemInfoProto.STATUS, mLastMemoryLevel);
+ proto.write(MemInfoProto.CACHED_PSS_KB, cachedPss);
+ proto.write(MemInfoProto.CACHED_KERNEL_KB, memInfo.getCachedSizeKb());
+ proto.write(MemInfoProto.FREE_KB, memInfo.getFreeSizeKb());
+ }
+ long lostRAM = memInfo.getTotalSizeKb() - (totalPss - totalSwapPss)
+ - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb()
+ - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb();
+ proto.write(MemInfoProto.USED_PSS_KB, totalPss - cachedPss);
+ proto.write(MemInfoProto.USED_KERNEL_KB, memInfo.getKernelUsedSizeKb());
+ proto.write(MemInfoProto.LOST_RAM_KB, lostRAM);
+ if (!brief) {
+ if (memInfo.getZramTotalSizeKb() != 0) {
+ proto.write(MemInfoProto.TOTAL_ZRAM_KB, memInfo.getZramTotalSizeKb());
+ proto.write(MemInfoProto.ZRAM_PHYSICAL_USED_IN_SWAP_KB,
+ memInfo.getSwapTotalSizeKb() - memInfo.getSwapFreeSizeKb());
+ proto.write(MemInfoProto.TOTAL_ZRAM_SWAP_KB, memInfo.getSwapTotalSizeKb());
+ }
+ final long[] ksm = getKsmInfo();
+ proto.write(MemInfoProto.KSM_SHARING_KB, ksm[KSM_SHARING]);
+ proto.write(MemInfoProto.KSM_SHARED_KB, ksm[KSM_SHARED]);
+ proto.write(MemInfoProto.KSM_UNSHARED_KB, ksm[KSM_UNSHARED]);
+ proto.write(MemInfoProto.KSM_VOLATILE_KB, ksm[KSM_VOLATILE]);
+
+ proto.write(MemInfoProto.TUNING_MB, ActivityManager.staticGetMemoryClass());
+ proto.write(MemInfoProto.TUNING_LARGE_MB, ActivityManager.staticGetLargeMemoryClass());
+ proto.write(MemInfoProto.OOM_KB,
+ mProcessList.getMemLevel(ProcessList.CACHED_APP_MAX_ADJ) / 1024);
+ proto.write(MemInfoProto.RESTORE_LIMIT_KB,
+ mProcessList.getCachedRestoreThresholdKb());
+
+ proto.write(MemInfoProto.IS_LOW_RAM_DEVICE, ActivityManager.isLowRamDeviceStatic());
+ proto.write(MemInfoProto.IS_HIGH_END_GFX, ActivityManager.isHighEndGfx());
+ }
+ }
proto.flush();
}
@@ -18269,7 +18881,7 @@ public class ActivityManagerService extends IActivityManager.Stub
for (int i = mPendingProcessChanges.size() - 1; i >= 0; i--) {
ProcessChangeItem item = mPendingProcessChanges.get(i);
- if (item.pid == app.pid) {
+ if (app.pid > 0 && item.pid == app.pid) {
mPendingProcessChanges.remove(i);
mAvailProcessChanges.add(item);
}
@@ -18321,6 +18933,7 @@ public class ActivityManagerService extends IActivityManager.Stub
ProcessList.remove(app.pid);
}
addProcessNameLocked(app);
+ app.pendingStart = false;
startProcessLocked(app, "restart", app.processName);
return true;
} else if (app.pid > 0 && app.pid != MY_PID) {
@@ -19267,16 +19880,14 @@ public class ActivityManagerService extends IActivityManager.Stub
userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
ALLOW_NON_FULL, "broadcast", callerPackage);
- // Make sure that the user who is receiving this broadcast is running.
- // If not, we will just skip it. Make an exception for shutdown broadcasts
- // and upgrade steps.
-
- if (userId != UserHandle.USER_ALL && !mUserController.isUserRunning(userId, 0)) {
+ // Make sure that the user who is receiving this broadcast or its parent is running.
+ // If not, we will just skip it. Make an exception for shutdown broadcasts, upgrade steps.
+ if (userId != UserHandle.USER_ALL && !mUserController.isUserOrItsParentRunning(userId)) {
if ((callingUid != SYSTEM_UID
|| (intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) == 0)
&& !Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {
Slog.w(TAG, "Skipping broadcast of " + intent
- + ": user " + userId + " is stopped");
+ + ": user " + userId + " and its parent (if any) are stopped");
return ActivityManager.BROADCAST_FAILED_USER_STOPPED;
}
}
@@ -20154,6 +20765,11 @@ public class ActivityManagerService extends IActivityManager.Stub
// Instrumentation can kill and relaunch even persistent processes
forceStopPackageLocked(ii.targetPackage, -1, true, false, true, true, false, userId,
"start instr");
+ // Inform usage stats to make the target package active
+ if (mUsageStatsService != null) {
+ mUsageStatsService.reportEvent(ii.targetPackage, userId,
+ UsageEvents.Event.SYSTEM_INTERACTION);
+ }
ProcessRecord app = addAppLocked(ai, defProcess, false, abiOverride);
app.instr = activeInstr;
activeInstr.mFinished = false;
@@ -20600,7 +21216,7 @@ public class ActivityManagerService extends IActivityManager.Stub
if (DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION, "Sending to proc "
+ app.processName + " new config " + configCopy);
mLifecycleManager.scheduleTransaction(app.thread,
- new ConfigurationChangeItem(configCopy));
+ ConfigurationChangeItem.obtain(configCopy));
}
} catch (Exception e) {
Slog.e(TAG_CONFIGURATION, "Failed to schedule configuration change", e);
@@ -21053,7 +21669,7 @@ public class ActivityManagerService extends IActivityManager.Stub
int procState;
boolean foregroundActivities = false;
mTmpBroadcastQueue.clear();
- if (app == TOP_APP) {
+ if (PROCESS_STATE_CUR_TOP == ActivityManager.PROCESS_STATE_TOP && app == TOP_APP) {
// The last app on the list is the foreground app.
adj = ProcessList.FOREGROUND_APP_ADJ;
schedGroup = ProcessList.SCHED_GROUP_TOP_APP;
@@ -21089,6 +21705,13 @@ public class ActivityManagerService extends IActivityManager.Stub
procState = ActivityManager.PROCESS_STATE_SERVICE;
if (DEBUG_OOM_ADJ_REASON) Slog.d(TAG, "Making exec-service: " + app);
//Slog.i(TAG, "EXEC " + (app.execServicesFg ? "FG" : "BG") + ": " + app);
+ } else if (app == TOP_APP) {
+ adj = ProcessList.FOREGROUND_APP_ADJ;
+ schedGroup = ProcessList.SCHED_GROUP_BACKGROUND;
+ app.adjType = "top-sleeping";
+ foregroundActivities = true;
+ procState = PROCESS_STATE_CUR_TOP;
+ if (DEBUG_OOM_ADJ_REASON) Slog.d(TAG, "Making top: " + app);
} else {
// As far as we know the process is empty. We may change our mind later.
schedGroup = ProcessList.SCHED_GROUP_BACKGROUND;
@@ -22537,7 +23160,7 @@ public class ActivityManagerService extends IActivityManager.Stub
if (app.curProcState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
isInteraction = true;
app.fgInteractionTime = 0;
- } else if (app.curProcState <= ActivityManager.PROCESS_STATE_TOP_SLEEPING) {
+ } else if (app.curProcState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
if (app.fgInteractionTime == 0) {
app.fgInteractionTime = nowElapsed;
isInteraction = false;
@@ -22971,7 +23594,8 @@ public class ActivityManagerService extends IActivityManager.Stub
break;
}
}
- } else if (app.curProcState == ActivityManager.PROCESS_STATE_HEAVY_WEIGHT) {
+ } else if (app.curProcState == ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
+ && !app.killedByAm) {
if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND
&& app.thread != null) {
try {
@@ -23508,7 +24132,7 @@ public class ActivityManagerService extends IActivityManager.Stub
+ ")\n");
if (app.pid > 0 && app.pid != MY_PID) {
app.kill("empty", false);
- } else {
+ } else if (app.thread != null) {
try {
app.thread.scheduleExit();
} catch (Exception e) {
@@ -23810,7 +24434,13 @@ public class ActivityManagerService extends IActivityManager.Stub
*/
@Override
public boolean startUserInBackground(final int userId) {
- return mUserController.startUser(userId, /* foreground */ false);
+ return startUserInBackgroundWithListener(userId, null);
+ }
+
+ @Override
+ public boolean startUserInBackgroundWithListener(final int userId,
+ @Nullable IProgressListener unlockListener) {
+ return mUserController.startUser(userId, /* foreground */ false, unlockListener);
}
@Override
@@ -24018,7 +24648,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public int startIsolatedProcess(String entryPoint, String[] entryPointArgs,
+ public boolean startIsolatedProcess(String entryPoint, String[] entryPointArgs,
String processName, String abiOverride, int uid, Runnable crashHandler) {
return ActivityManagerService.this.startIsolatedProcess(entryPoint, entryPointArgs,
processName, abiOverride, uid, crashHandler);
@@ -24180,8 +24810,8 @@ public class ActivityManagerService extends IActivityManager.Stub
}
synchronized (ActivityManagerService.this) {
- return startActivitiesInPackage(packageUid, packageName, intents, resolvedTypes,
- /*resultTo*/ null, bOptions, userId);
+ return mActivityStartController.startActivitiesInPackage(packageUid, packageName,
+ intents, resolvedTypes, /*resultTo*/ null, bOptions, userId);
}
}
@@ -24306,6 +24936,13 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
+ @Override
+ public void notifyActiveVoiceInteractionServiceChanged(ComponentName component) {
+ synchronized (ActivityManagerService.this) {
+ mActiveVoiceInteractionServiceComponent = component;
+ }
+ }
+
/**
* Called after virtual display Id is updated by
* {@link com.android.server.vr.Vr2dDisplay} with a specific
@@ -24332,7 +24969,7 @@ public class ActivityManagerService extends IActivityManager.Stub
pw.println(" Reason: " + reason);
}
pw.println();
- mActivityStarter.dump(pw, " ", null);
+ mActivityStartController.dump(pw, " ", null);
pw.println();
pw.println("-------------------------------------------------------------------------------");
dumpActivitiesLocked(null /* fd */, pw, null /* args */, 0 /* opti */,
@@ -24392,6 +25029,31 @@ public class ActivityManagerService extends IActivityManager.Stub
public boolean isRuntimeRestarted() {
return mSystemServiceManager.isRuntimeRestarted();
}
+
+ @Override
+ public boolean hasRunningActivity(int uid, @Nullable String packageName) {
+ if (packageName == null) return false;
+
+ synchronized (ActivityManagerService.this) {
+ for (int i = 0; i < mLruProcesses.size(); i++) {
+ final ProcessRecord processRecord = mLruProcesses.get(i);
+ if (processRecord.uid == uid) {
+ for (int j = 0; j < processRecord.activities.size(); j++) {
+ final ActivityRecord activityRecord = processRecord.activities.get(j);
+ if (packageName.equals(activityRecord.packageName)) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void registerScreenObserver(ScreenObserver observer) {
+ mScreenObservers.add(observer);
+ }
}
/**
diff --git a/com/android/server/am/ActivityManagerShellCommand.java b/com/android/server/am/ActivityManagerShellCommand.java
index 7eb922c9..4f60e173 100644
--- a/com/android/server/am/ActivityManagerShellCommand.java
+++ b/com/android/server/am/ActivityManagerShellCommand.java
@@ -48,6 +48,7 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ShellCommand;
+import android.os.StrictMode;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
@@ -69,7 +70,9 @@ import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import static android.app.ActivityManager.RESIZE_MODE_SYSTEM;
import static android.app.ActivityManager.RESIZE_MODE_USER;
@@ -126,7 +129,7 @@ final class ActivityManagerShellCommand extends ShellCommand {
if (cmd == null) {
return handleDefaultCommands(cmd);
}
- PrintWriter pw = getOutPrintWriter();
+ final PrintWriter pw = getOutPrintWriter();
try {
switch (cmd) {
case "start":
@@ -224,6 +227,8 @@ final class ActivityManagerShellCommand extends ShellCommand {
return runGetInactive(pw);
case "set-standby-bucket":
return runSetStandbyBucket(pw);
+ case "get-standby-bucket":
+ return runGetStandbyBucket(pw);
case "send-trim-memory":
return runSendTrimMemory(pw);
case "display":
@@ -1324,65 +1329,95 @@ final class ActivityManagerShellCommand extends ShellCommand {
@Override
public void onUidStateChanged(int uid, int procState, long procStateSeq) throws RemoteException {
synchronized (this) {
- mPw.print(uid);
- mPw.print(" procstate ");
- mPw.print(ProcessList.makeProcStateString(procState));
- mPw.print(" seq ");
- mPw.println(procStateSeq);
- mPw.flush();
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ mPw.print(uid);
+ mPw.print(" procstate ");
+ mPw.print(ProcessList.makeProcStateString(procState));
+ mPw.print(" seq ");
+ mPw.println(procStateSeq);
+ mPw.flush();
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
}
}
@Override
public void onUidGone(int uid, boolean disabled) throws RemoteException {
synchronized (this) {
- mPw.print(uid);
- mPw.print(" gone");
- if (disabled) {
- mPw.print(" disabled");
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ mPw.print(uid);
+ mPw.print(" gone");
+ if (disabled) {
+ mPw.print(" disabled");
+ }
+ mPw.println();
+ mPw.flush();
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
}
- mPw.println();
- mPw.flush();
}
}
@Override
public void onUidActive(int uid) throws RemoteException {
synchronized (this) {
- mPw.print(uid);
- mPw.println(" active");
- mPw.flush();
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ mPw.print(uid);
+ mPw.println(" active");
+ mPw.flush();
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
}
}
@Override
public void onUidIdle(int uid, boolean disabled) throws RemoteException {
synchronized (this) {
- mPw.print(uid);
- mPw.print(" idle");
- if (disabled) {
- mPw.print(" disabled");
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ mPw.print(uid);
+ mPw.print(" idle");
+ if (disabled) {
+ mPw.print(" disabled");
+ }
+ mPw.println();
+ mPw.flush();
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
}
- mPw.println();
- mPw.flush();
}
}
@Override
public void onUidCachedChanged(int uid, boolean cached) throws RemoteException {
synchronized (this) {
- mPw.print(uid);
- mPw.println(cached ? " cached" : " uncached");
- mPw.flush();
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ mPw.print(uid);
+ mPw.println(cached ? " cached" : " uncached");
+ mPw.flush();
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
}
}
@Override
public void onOomAdjMessage(String msg) {
synchronized (this) {
- mPw.print("# ");
- mPw.println(msg);
- mPw.flush();
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ mPw.print("# ");
+ mPw.println(msg);
+ mPw.flush();
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
}
}
@@ -1826,6 +1861,29 @@ final class ActivityManagerShellCommand extends ShellCommand {
return 0;
}
+ private int bucketNameToBucketValue(String name) {
+ String lower = name.toLowerCase();
+ if (lower.startsWith("ac")) {
+ return UsageStatsManager.STANDBY_BUCKET_ACTIVE;
+ } else if (lower.startsWith("wo")) {
+ return UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
+ } else if (lower.startsWith("fr")) {
+ return UsageStatsManager.STANDBY_BUCKET_FREQUENT;
+ } else if (lower.startsWith("ra")) {
+ return UsageStatsManager.STANDBY_BUCKET_RARE;
+ } else if (lower.startsWith("ne")) {
+ return UsageStatsManager.STANDBY_BUCKET_NEVER;
+ } else {
+ try {
+ int bucket = Integer.parseInt(lower);
+ return bucket;
+ } catch (NumberFormatException nfe) {
+ getErrPrintWriter().println("Error: Unknown bucket: " + name);
+ }
+ }
+ return -1;
+ }
+
int runSetStandbyBucket(PrintWriter pw) throws RemoteException {
int userId = UserHandle.USER_CURRENT;
@@ -1840,10 +1898,56 @@ final class ActivityManagerShellCommand extends ShellCommand {
}
String packageName = getNextArgRequired();
String value = getNextArgRequired();
+ int bucket = bucketNameToBucketValue(value);
+ if (bucket < 0) return -1;
+ boolean multiple = peekNextArg() != null;
+
+
+ IUsageStatsManager usm = IUsageStatsManager.Stub.asInterface(ServiceManager.getService(
+ Context.USAGE_STATS_SERVICE));
+ if (!multiple) {
+ usm.setAppStandbyBucket(packageName, bucketNameToBucketValue(value), userId);
+ } else {
+ HashMap<String, Integer> buckets = new HashMap<>();
+ buckets.put(packageName, bucket);
+ while ((packageName = getNextArg()) != null) {
+ value = getNextArgRequired();
+ bucket = bucketNameToBucketValue(value);
+ if (bucket < 0) continue;
+ buckets.put(packageName, bucket);
+ }
+ usm.setAppStandbyBuckets(buckets, userId);
+ }
+ return 0;
+ }
+
+ int runGetStandbyBucket(PrintWriter pw) throws RemoteException {
+ int userId = UserHandle.USER_CURRENT;
+
+ String opt;
+ while ((opt=getNextOption()) != null) {
+ if (opt.equals("--user")) {
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ } else {
+ getErrPrintWriter().println("Error: Unknown option: " + opt);
+ return -1;
+ }
+ }
+ String packageName = getNextArg();
IUsageStatsManager usm = IUsageStatsManager.Stub.asInterface(ServiceManager.getService(
Context.USAGE_STATS_SERVICE));
- usm.setAppStandbyBucket(packageName, Integer.parseInt(value), userId);
+ if (packageName != null) {
+ int bucket = usm.getAppStandbyBucket(packageName, null, userId);
+ pw.println(bucket);
+ } else {
+ Map<String, Integer> buckets = (Map<String, Integer>) usm.getAppStandbyBuckets(
+ SHELL_PACKAGE_NAME, userId);
+ for (Map.Entry<String, Integer> entry: buckets.entrySet()) {
+ pw.print(entry.getKey()); pw.print(": ");
+ pw.println(entry.getValue());
+ }
+ }
return 0;
}
@@ -2597,8 +2701,10 @@ final class ActivityManagerShellCommand extends ShellCommand {
pw.println(" Sets the inactive state of an app.");
pw.println(" get-inactive [--user <USER_ID>] <PACKAGE>");
pw.println(" Returns the inactive state of an app.");
- pw.println(" set-standby-bucket [--user <USER_ID>] <PACKAGE> <BUCKET>");
+ pw.println(" set-standby-bucket [--user <USER_ID>] <PACKAGE> active|working_set|frequent|rare");
pw.println(" Puts an app in the standby bucket.");
+ pw.println(" get-standby-bucket [--user <USER_ID>] <PACKAGE>");
+ pw.println(" Returns the standby bucket of an app.");
pw.println(" send-trim-memory [--user <USER_ID>] <PROCESS>");
pw.println(" [HIDDEN|RUNNING_MODERATE|BACKGROUND|RUNNING_LOW|MODERATE|RUNNING_CRITICAL|COMPLETE]");
pw.println(" Send a memory trim event to a <PROCESS>. May also supply a raw trim int level.");
diff --git a/com/android/server/am/ActivityRecord.java b/com/android/server/am/ActivityRecord.java
index a089e6ce..8eb51979 100644
--- a/com/android/server/am/ActivityRecord.java
+++ b/com/android/server/am/ActivityRecord.java
@@ -23,6 +23,7 @@ import static android.app.ActivityOptions.ANIM_CLIP_REVEAL;
import static android.app.ActivityOptions.ANIM_CUSTOM;
import static android.app.ActivityOptions.ANIM_SCALE_UP;
import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION;
+import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
import static android.app.ActivityOptions.ANIM_THUMBNAIL_ASPECT_SCALE_DOWN;
import static android.app.ActivityOptions.ANIM_THUMBNAIL_ASPECT_SCALE_UP;
import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN;
@@ -618,7 +619,7 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
+ ", displayId=" + displayId + ", config=" + config);
service.mLifecycleManager.scheduleTransaction(app.thread, appToken,
- new MoveToDisplayItem(displayId, config));
+ MoveToDisplayItem.obtain(displayId, config));
} catch (RemoteException e) {
// If process died, whatever.
}
@@ -636,7 +637,7 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
+ config);
service.mLifecycleManager.scheduleTransaction(app.thread, appToken,
- new ActivityConfigurationChangeItem(config));
+ ActivityConfigurationChangeItem.obtain(config));
} catch (RemoteException e) {
// If process died, whatever.
}
@@ -663,7 +664,7 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
private void scheduleMultiWindowModeChanged(Configuration overrideConfig) {
try {
service.mLifecycleManager.scheduleTransaction(app.thread, appToken,
- new MultiWindowModeChangeItem(mLastReportedMultiWindowMode,
+ MultiWindowModeChangeItem.obtain(mLastReportedMultiWindowMode,
overrideConfig));
} catch (Exception e) {
// If process died, I don't care.
@@ -691,7 +692,7 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
private void schedulePictureInPictureModeChanged(Configuration overrideConfig) {
try {
service.mLifecycleManager.scheduleTransaction(app.thread, appToken,
- new PipModeChangeItem(mLastReportedPictureInPictureMode,
+ PipModeChangeItem.obtain(mLastReportedPictureInPictureMode,
overrideConfig));
} catch (Exception e) {
// If process died, no one cares.
@@ -1050,11 +1051,7 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
* @return whether the given package name can launch an assist activity.
*/
private boolean canLaunchAssistActivity(String packageName) {
- if (service.mAssistUtils == null) {
- return false;
- }
-
- final ComponentName assistComponent = service.mAssistUtils.getActiveServiceComponentName();
+ final ComponentName assistComponent = service.mActiveVoiceInteractionServiceComponent;
if (assistComponent != null) {
return assistComponent.getPackageName().equals(packageName);
}
@@ -1380,7 +1377,7 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
ArrayList<ReferrerIntent> ar = new ArrayList<>(1);
ar.add(rintent);
service.mLifecycleManager.scheduleTransaction(app.thread, appToken,
- new NewIntentItem(ar, state == PAUSED));
+ NewIntentItem.obtain(ar, state == PAUSED));
unsent = false;
} catch (RemoteException e) {
Slog.w(TAG, "Exception thrown sending new intent to " + this, e);
@@ -1480,6 +1477,9 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
}
}
break;
+ case ANIM_OPEN_CROSS_PROFILE_APPS:
+ service.mWindowManager.overridePendingAppTransitionStartCrossProfileApps();
+ break;
default:
Slog.e(TAG, "applyOptionsLocked: Unknown animationType=" + animationType);
break;
@@ -1603,7 +1603,7 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
sleeping = false;
app.pendingUiClean = true;
service.mLifecycleManager.scheduleTransaction(app.thread, appToken,
- new WindowVisibilityItem(true /* showWindow */));
+ WindowVisibilityItem.obtain(true /* showWindow */));
// The activity may be waiting for stop, but that is no longer appropriate for it.
mStackSupervisor.mStoppingActivities.remove(this);
mStackSupervisor.mGoingToSleepActivities.remove(this);
@@ -2735,7 +2735,8 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
* {@link #mShowWhenLocked}.
*/
boolean canShowWhenLocked() {
- return mShowWhenLocked || service.mWindowManager.containsShowWhenLockedWindow(appToken);
+ return !inMultiWindowMode() && (mShowWhenLocked
+ || service.mWindowManager.containsShowWhenLockedWindow(appToken));
}
void setTurnScreenOn(boolean turnScreenOn) {
diff --git a/com/android/server/am/ActivityStack.java b/com/android/server/am/ActivityStack.java
index bdfd82f4..10c801da 100644
--- a/com/android/server/am/ActivityStack.java
+++ b/com/android/server/am/ActivityStack.java
@@ -144,6 +144,7 @@ import com.android.internal.app.IVoiceInteractor;
import com.android.internal.os.BatteryStatsImpl;
import com.android.server.Watchdog;
import com.android.server.am.ActivityManagerService.ItemMatcher;
+import com.android.server.am.EventLogTags;
import com.android.server.wm.ConfigurationContainer;
import com.android.server.wm.StackWindowController;
import com.android.server.wm.StackWindowListener;
@@ -482,19 +483,25 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
@Override
public void setWindowingMode(int windowingMode) {
- setWindowingMode(windowingMode, false /* animate */);
+ setWindowingMode(windowingMode, false /* animate */, true /* showRecents */,
+ false /* enteringSplitScreenMode */);
}
- void setWindowingMode(int preferredWindowingMode, boolean animate) {
+ void setWindowingMode(int preferredWindowingMode, boolean animate, boolean showRecents,
+ boolean enteringSplitScreenMode) {
+ final boolean creating = mWindowContainerController == null;
final int currentMode = getWindowingMode();
final ActivityDisplay display = getDisplay();
final TaskRecord topTask = topTask();
final ActivityStack splitScreenStack = display.getSplitScreenPrimaryStack();
mTmpOptions.setLaunchWindowingMode(preferredWindowingMode);
- // Need to make sure windowing mode is supported.
- int windowingMode = display.resolveWindowingMode(
- null /* ActivityRecord */, mTmpOptions, topTask, getActivityType());
+ // Need to make sure windowing mode is supported. If we in the process of creating the stack
+ // no need to resolve the windowing mode again as it is already resolved to the right mode.
+ int windowingMode = creating
+ ? preferredWindowingMode
+ : display.resolveWindowingMode(
+ null /* ActivityRecord */, mTmpOptions, topTask, getActivityType());
if (splitScreenStack == this && windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
// Resolution to split-screen secondary for the primary split-screen stack means we want
// to go fullscreen.
@@ -503,13 +510,19 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
final boolean alreadyInSplitScreenMode = display.hasSplitScreenPrimaryStack();
+ // Don't send non-resizeable notifications if the windowing mode changed was a side effect
+ // of us entering split-screen mode.
+ final boolean sendNonResizeableNotification = !enteringSplitScreenMode;
// Take any required action due to us not supporting the preferred windowing mode.
- if (windowingMode != preferredWindowingMode && isActivityTypeStandardOrUndefined()) {
- if (alreadyInSplitScreenMode
- && (preferredWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
- || preferredWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY)) {
- // Looks like we can't launch in split screen mode, go ahead an dismiss split-screen
- // and display a warning toast about it.
+ if (alreadyInSplitScreenMode && windowingMode == WINDOWING_MODE_FULLSCREEN
+ && sendNonResizeableNotification && isActivityTypeStandardOrUndefined()) {
+ final boolean preferredSplitScreen =
+ preferredWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
+ || preferredWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+ if (preferredSplitScreen || creating) {
+ // Looks like we can't launch in split screen mode or the stack we are launching
+ // doesn't support split-screen mode, go ahead an dismiss split-screen and display a
+ // warning toast about it.
mService.mTaskChangeNotificationController.notifyActivityDismissingDockedStack();
display.getSplitScreenPrimaryStack().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
}
@@ -523,8 +536,9 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
final WindowManagerService wm = mService.mWindowManager;
final ActivityRecord topActivity = getTopActivity();
- if (windowingMode != WINDOWING_MODE_FULLSCREEN && topActivity != null
- && topActivity.isNonResizableOrForcedResizable() && !topActivity.noDisplay) {
+ if (sendNonResizeableNotification && windowingMode != WINDOWING_MODE_FULLSCREEN
+ && topActivity != null && topActivity.isNonResizableOrForcedResizable()
+ && !topActivity.noDisplay) {
// Inform the user that they are starting an app that may not work correctly in
// multi-window mode.
final String packageName = topActivity.appInfo.packageName;
@@ -539,7 +553,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
}
super.setWindowingMode(windowingMode);
- if (mWindowContainerController == null) {
+ if (creating) {
// Nothing else to do if we don't have a window container yet. E.g. call from ctor.
return;
}
@@ -579,7 +593,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
resize(mTmpRect2, null /* tempTaskBounds */, null /* tempTaskInsetBounds */);
}
} finally {
- if (!alreadyInSplitScreenMode && mDisplayId == DEFAULT_DISPLAY
+ if (showRecents && !alreadyInSplitScreenMode && mDisplayId == DEFAULT_DISPLAY
&& windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
// Make sure recents stack exist when creating a dock stack as it normally needs to
// be on the other side of the docked stack and we make visibility decisions based
@@ -589,16 +603,22 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
// the one where the home stack is visible since recents isn't visible yet, but the
// divider will be off. I think we should just make the initial bounds that of home
// so that the divider matches and remove this logic.
- display.getOrCreateStack(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY,
- ACTIVITY_TYPE_RECENTS, true /* onTop */);
+ final ActivityStack recentStack = display.getOrCreateStack(
+ WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, ACTIVITY_TYPE_RECENTS,
+ true /* onTop */);
+ recentStack.moveToFront("setWindowingMode");
// If task moved to docked stack - show recents if needed.
mService.mWindowManager.showRecentApps(false /* fromHome */);
}
wm.continueSurfaceLayout();
}
- mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, PRESERVE_WINDOWS);
- mStackSupervisor.resumeFocusedStackTopActivityLocked();
+ // Don't ensure visible activities if the windowing mode change was a side effect of us
+ // entering split-screen mode.
+ if (!enteringSplitScreenMode) {
+ mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, PRESERVE_WINDOWS);
+ mStackSupervisor.resumeFocusedStackTopActivityLocked();
+ }
}
@Override
@@ -1424,13 +1444,12 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
if (prev.app != null && prev.app.thread != null) {
if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Enqueueing pending pause: " + prev);
try {
- EventLog.writeEvent(EventLogTags.AM_PAUSE_ACTIVITY,
- prev.userId, System.identityHashCode(prev),
- prev.shortComponentName);
+ EventLogTags.writeAmPauseActivity(prev.userId, System.identityHashCode(prev),
+ prev.shortComponentName, "userLeaving=" + userLeaving);
mService.updateUsageStats(prev, false);
mService.mLifecycleManager.scheduleTransaction(prev.app.thread, prev.appToken,
- new PauseActivityItem(prev.finishing, userLeaving,
+ PauseActivityItem.obtain(prev.finishing, userLeaving,
prev.configChangeFlags, pauseImmediately));
} catch (Exception e) {
// Ignore exception, if process died other code will cleanup.
@@ -1664,7 +1683,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
continue;
}
- if (!r.visible && r != starting) {
+ if (!r.visibleIgnoringKeyguard && r != starting) {
// Also ignore invisible activities that are not the currently starting
// activity (about to be visible).
continue;
@@ -1792,6 +1811,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
boolean behindFullscreenActivity = !stackShouldBeVisible;
boolean resumeNextActivity = mStackSupervisor.isFocusedStack(this)
&& (isInStackLocked(starting) == null);
+ final boolean isTopFullscreenStack = getDisplay().isTopFullscreenStack(this);
for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) {
final TaskRecord task = mTaskHistory.get(taskNdx);
final ArrayList<ActivityRecord> activities = task.mActivities;
@@ -1813,7 +1833,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
// Now check whether it's really visible depending on Keyguard state.
final boolean reallyVisible = checkKeyguardVisibility(r,
- visibleIgnoringKeyguard, isTop);
+ visibleIgnoringKeyguard, isTop && isTopFullscreenStack);
if (visibleIgnoringKeyguard) {
behindFullscreenActivity = updateBehindFullscreen(!stackShouldBeVisible,
behindFullscreenActivity, r);
@@ -1939,13 +1959,11 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
*
* @return true if {@param r} is visible taken Keyguard state into account, false otherwise
*/
- boolean checkKeyguardVisibility(ActivityRecord r, boolean shouldBeVisible,
- boolean isTop) {
- final boolean isInPinnedStack = r.inPinnedWindowingMode();
+ boolean checkKeyguardVisibility(ActivityRecord r, boolean shouldBeVisible, boolean isTop) {
final boolean keyguardShowing = mStackSupervisor.getKeyguardController().isKeyguardShowing(
mDisplayId != INVALID_DISPLAY ? mDisplayId : DEFAULT_DISPLAY);
final boolean keyguardLocked = mStackSupervisor.getKeyguardController().isKeyguardLocked();
- final boolean showWhenLocked = r.canShowWhenLocked() && !isInPinnedStack;
+ final boolean showWhenLocked = r.canShowWhenLocked();
final boolean dismissKeyguard = r.hasDismissKeyguardWindows();
if (shouldBeVisible) {
if (dismissKeyguard && mTopDismissingKeyguardActivity == null) {
@@ -2061,7 +2079,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
if (DEBUG_VISIBILITY) Slog.v(TAG_VISIBILITY,
"Scheduling invisibility: " + r);
mService.mLifecycleManager.scheduleTransaction(r.app.thread, r.appToken,
- new WindowVisibilityItem(false /* showWindow */));
+ WindowVisibilityItem.obtain(false /* showWindow */));
}
// Reset the flag indicating that an app can enter picture-in-picture once the
@@ -2260,7 +2278,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
// Remember how we'll process this pause/resume situation, and ensure
// that the state is reset however we wind up proceeding.
- final boolean userLeaving = mStackSupervisor.mUserLeaving;
+ boolean userLeaving = mStackSupervisor.mUserLeaving;
mStackSupervisor.mUserLeaving = false;
if (!hasRunningActivity) {
@@ -2331,6 +2349,13 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
// So, why aren't we using prev here??? See the param comment on the method. prev doesn't
// represent the last resumed activity. However, the last focus stack does if it isn't null.
final ActivityRecord lastResumed = lastFocusedStack.mResumedActivity;
+ if (userLeaving && inMultiWindowMode() && lastFocusedStack.shouldBeVisible(next)) {
+ // The user isn't leaving if this stack is the multi-window mode and the last
+ // focused stack should still be visible.
+ if(DEBUG_USER_LEAVING) Slog.i(TAG_USER_LEAVING, "Overriding userLeaving to false"
+ + " next=" + next + " lastResumed=" + lastResumed);
+ userLeaving = false;
+ }
lastResumedCanPip = lastResumed != null && lastResumed.checkEnterPictureInPictureState(
"resumeTopActivity", userLeaving /* beforeStopping */);
}
@@ -2582,13 +2607,13 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
if (DEBUG_RESULTS) Slog.v(TAG_RESULTS,
"Delivering results to " + next + ": " + a);
mService.mLifecycleManager.scheduleTransaction(next.app.thread,
- next.appToken, new ActivityResultItem(a));
+ next.appToken, ActivityResultItem.obtain(a));
}
}
if (next.newIntents != null) {
mService.mLifecycleManager.scheduleTransaction(next.app.thread,
- next.appToken, new NewIntentItem(next.newIntents,
+ next.appToken, NewIntentItem.obtain(next.newIntents,
false /* andPause */));
}
@@ -2607,7 +2632,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
next.app.forceProcessStateUpTo(mService.mTopProcessState);
next.clearOptionsLocked();
mService.mLifecycleManager.scheduleTransaction(next.app.thread, next.appToken,
- new ResumeActivityItem(next.app.repProcState,
+ ResumeActivityItem.obtain(next.app.repProcState,
mService.isNextTransitionForward()));
if (DEBUG_STATES) Slog.d(TAG_STATES, "resumeTopActivityLocked: Resumed "
@@ -3259,7 +3284,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
list.add(new ResultInfo(resultWho, requestCode,
resultCode, data));
mService.mLifecycleManager.scheduleTransaction(r.app.thread, r.appToken,
- new ActivityResultItem(list));
+ ActivityResultItem.obtain(list));
return;
} catch (Exception e) {
Slog.w(TAG, "Exception thrown sending result to " + r, e);
@@ -3388,7 +3413,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
EventLogTags.writeAmStopActivity(
r.userId, System.identityHashCode(r), r.shortComponentName);
mService.mLifecycleManager.scheduleTransaction(r.app.thread, r.appToken,
- new StopActivityItem(r.visible, r.configChangeFlags));
+ StopActivityItem.obtain(r.visible, r.configChangeFlags));
if (shouldSleepOrShutDownActivities()) {
r.setSleeping(true);
}
@@ -3892,11 +3917,19 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
try {
ActivityInfo aInfo = AppGlobals.getPackageManager().getActivityInfo(
destIntent.getComponent(), 0, srec.userId);
- int res = mService.mActivityStarter.startActivityLocked(srec.app.thread,
- destIntent, null /*ephemeralIntent*/, null, aInfo, null /*rInfo*/, null,
- null, parent.appToken, null, 0, -1, parent.launchedFromUid,
- parent.launchedFromPackage, -1, parent.launchedFromUid, 0, null,
- false, true, null, null, "navigateUpTo");
+ // TODO(b/64750076): Check if calling pid should really be -1.
+ final int res = mService.getActivityStartController()
+ .obtainStarter(destIntent, "navigateUpTo")
+ .setCaller(srec.app.thread)
+ .setActivityInfo(aInfo)
+ .setResultTo(parent.appToken)
+ .setCallingPid(-1)
+ .setCallingUid(parent.launchedFromUid)
+ .setCallingPackage(parent.launchedFromPackage)
+ .setRealCallingPid(-1)
+ .setRealCallingUid(parent.launchedFromUid)
+ .setComponentSpecified(true)
+ .execute();
foundParentInTask = res == ActivityManager.START_SUCCESS;
} catch (RemoteException e) {
foundParentInTask = false;
@@ -4186,7 +4219,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
try {
if (DEBUG_SWITCH) Slog.i(TAG_SWITCH, "Destroying: " + r);
mService.mLifecycleManager.scheduleTransaction(r.app.thread, r.appToken,
- new DestroyActivityItem(r.finishing, r.configChangeFlags));
+ DestroyActivityItem.obtain(r.finishing, r.configChangeFlags));
} catch (Exception e) {
// We can just ignore exceptions here... if the process
// has crashed, our death notification will clean things
@@ -5007,8 +5040,8 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
boolean toTop, ActivityRecord activity, ActivityRecord source,
ActivityOptions options) {
- final TaskRecord task = new TaskRecord(mService, taskId, info, intent, voiceSession,
- voiceInteractor);
+ final TaskRecord task = TaskRecord.create(
+ mService, taskId, info, intent, voiceSession, voiceInteractor);
// add the task to stack first, mTaskPositioner might need the stack association
addTask(task, toTop, "createTaskRecord");
final boolean isLockscreenShown = mService.mStackSupervisor.getKeyguardController()
diff --git a/com/android/server/am/ActivityStackSupervisor.java b/com/android/server/am/ActivityStackSupervisor.java
index 445bf679..0a42aa9c 100644
--- a/com/android/server/am/ActivityStackSupervisor.java
+++ b/com/android/server/am/ActivityStackSupervisor.java
@@ -114,7 +114,11 @@ import android.app.ResultInfo;
import android.app.WaitResult;
import android.app.WindowConfiguration.ActivityType;
import android.app.WindowConfiguration.WindowingMode;
+import android.app.servertransaction.ActivityLifecycleItem;
+import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.LaunchActivityItem;
+import android.app.servertransaction.PauseActivityItem;
+import android.app.servertransaction.ResumeActivityItem;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -129,7 +133,7 @@ import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.hardware.display.DisplayManagerInternal;
-import android.hardware.input.InputManagerInternal;
+import android.hardware.power.V1_0.PowerHint;
import android.os.Binder;
import android.os.Bundle;
import android.os.Debug;
@@ -298,9 +302,6 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
private LaunchingBoundsController mLaunchingBoundsController;
- /** Counter for next free stack ID to use for dynamic activity stacks. */
- private int mNextFreeStackId = 0;
-
/**
* Maps the task identifier that activities are currently being started in to the userId of the
* task. Each time a new task is created, the entry for the userId of the task is incremented
@@ -364,6 +365,9 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
* is being brought in front of us. */
boolean mUserLeaving = false;
+ /** Set when a power hint has started, but not ended. */
+ private boolean mPowerHintSent;
+
/**
* We don't want to allow the device to go to sleep while in the process
* of launching an activity. This is primarily to allow alarm intent
@@ -978,7 +982,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
}
// Send launch end powerhint when idle
- mService.mActivityStarter.sendPowerHintForLaunchEndIfNeeded();
+ sendPowerHintForLaunchEndIfNeeded();
return true;
}
@@ -1235,9 +1239,16 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId, int flags) {
synchronized (mService) {
- return mService.getPackageManagerInternalLocked().resolveIntent(intent, resolvedType,
- PackageManager.MATCH_INSTANT | PackageManager.MATCH_DEFAULT_ONLY | flags
- | ActivityManagerService.STOCK_PM_FLAGS, userId, true);
+ try {
+ Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "resolveIntent");
+ return mService.getPackageManagerInternalLocked().resolveIntent(
+ intent, resolvedType, PackageManager.MATCH_INSTANT
+ | PackageManager.MATCH_DEFAULT_ONLY | flags
+ | ActivityManagerService.STOCK_PM_FLAGS, userId, true);
+
+ } finally {
+ Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
+ }
}
}
@@ -1393,16 +1404,33 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
r.setLastReportedConfiguration(mergedConfiguration);
logIfTransactionTooLarge(r.intent, r.icicle);
- mService.mLifecycleManager.scheduleTransaction(app.thread, r.appToken,
- new LaunchActivityItem(new Intent(r.intent),
+
+
+ // Create activity launch transaction.
+ final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread,
+ r.appToken);
+ clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
System.identityHashCode(r), r.info,
// TODO: Have this take the merged configuration instead of separate global
// and override configs.
mergedConfiguration.getGlobalConfiguration(),
mergedConfiguration.getOverrideConfiguration(), r.compat,
r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
- r.persistentState, results, newIntents, !andResume,
- mService.isNextTransitionForward(), profilerInfo));
+ r.persistentState, results, newIntents, mService.isNextTransitionForward(),
+ profilerInfo));
+
+ // Set desired final state.
+ final ActivityLifecycleItem lifecycleItem;
+ if (andResume) {
+ lifecycleItem = ResumeActivityItem.obtain(mService.isNextTransitionForward());
+ } else {
+ lifecycleItem = PauseActivityItem.obtain();
+ }
+ clientTransaction.setLifecycleStateRequest(lifecycleItem);
+
+ // Schedule transaction.
+ mService.mLifecycleManager.scheduleTransaction(clientTransaction);
+
if ((app.info.privateFlags & ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) {
// This may be a heavy-weight process! Note that the package
@@ -1470,7 +1498,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
// a chance to initialize itself while in the background, making the
// switch back to it faster and look better.
if (isFocusedStack(stack)) {
- mService.startSetupActivityLocked();
+ mService.getActivityStartController().startSetupActivity();
}
// Update any services we are bound to that might care about whether
@@ -1531,6 +1559,32 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
"activity", r.intent.getComponent(), false, false, true);
}
+ void sendPowerHintForLaunchStartIfNeeded(boolean forceSend, ActivityRecord targetActivity) {
+ boolean sendHint = forceSend;
+
+ if (!sendHint) {
+ // If not forced, send power hint when the activity's process is different than the
+ // current resumed activity.
+ final ActivityRecord resumedActivity = getResumedActivityLocked();
+ sendHint = resumedActivity == null
+ || resumedActivity.app == null
+ || !resumedActivity.app.equals(targetActivity.app);
+ }
+
+ if (sendHint && mService.mLocalPowerManager != null) {
+ mService.mLocalPowerManager.powerHint(PowerHint.LAUNCH, 1);
+ mPowerHintSent = true;
+ }
+ }
+
+ void sendPowerHintForLaunchEndIfNeeded() {
+ // Trigger launch power hint if activity is launched
+ if (mPowerHintSent && mService.mLocalPowerManager != null) {
+ mService.mLocalPowerManager.powerHint(PowerHint.LAUNCH, 0);
+ mPowerHintSent = false;
+ }
+ }
+
boolean checkStartAnyActivityPermission(Intent intent, ActivityInfo aInfo,
String resultWho, int requestCode, int callingPid, int callingUid,
String callingPackage, boolean ignoreTargetSecurity, ProcessRecord callerApp,
@@ -2258,7 +2312,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
if (displayId == INVALID_DISPLAY) {
displayId = candidateDisplayId;
}
- if (displayId != INVALID_DISPLAY) {
+ if (displayId != INVALID_DISPLAY && canLaunchOnDisplay(r, displayId)) {
if (r != null) {
// TODO: This should also take in the windowing mode and activity type into account.
stack = (T) getValidLaunchStackOnDisplay(displayId, r);
@@ -2287,7 +2341,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
if (stack != null) {
display = stack.getDisplay();
- if (display != null) {
+ if (display != null && canLaunchOnDisplay(r, display.mDisplayId)) {
final int windowingMode =
display.resolveWindowingMode(r, options, candidateTask, activityType);
if (stack.isCompatible(windowingMode, activityType)) {
@@ -2297,6 +2351,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
if (display == null
+ || !canLaunchOnDisplay(r, display.mDisplayId)
// TODO: Can be removed once we figure-out how non-standard types should launch
// outside the default display.
|| (activityType != ACTIVITY_TYPE_STANDARD
@@ -2307,6 +2362,14 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
return display.getOrCreateStack(r, options, candidateTask, activityType, onTop);
}
+ /** @return true if activity record is null or can be launched on provided display. */
+ private boolean canLaunchOnDisplay(ActivityRecord r, int displayId) {
+ if (r == null) {
+ return true;
+ }
+ return r.canBeLaunchedOnDisplay(displayId);
+ }
+
/**
* Get a topmost stack on the display, that is a valid launch stack for specified activity.
* If there is no such stack, new dynamic stack can be created.
@@ -2481,14 +2544,14 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
}
- private void deferUpdateBounds(int activityType) {
+ void deferUpdateBounds(int activityType) {
final ActivityStack stack = getStack(WINDOWING_MODE_UNDEFINED, activityType);
if (stack != null) {
stack.deferUpdateBounds();
}
}
- private void continueUpdateBounds(int activityType) {
+ void continueUpdateBounds(int activityType) {
final ActivityStack stack = getStack(WINDOWING_MODE_UNDEFINED, activityType);
if (stack != null) {
stack.continueUpdateBounds();
@@ -2875,16 +2938,6 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
}
- int getNextStackId() {
- while (true) {
- if (getStack(mNextFreeStackId) == null) {
- break;
- }
- mNextFreeStackId++;
- }
- return mNextFreeStackId;
- }
-
/**
* Called to restore the state of the task into the stack that it's supposed to go into.
*
@@ -3335,7 +3388,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
// Send launch end powerhint before going sleep
- mService.mActivityStarter.sendPowerHintForLaunchEndIfNeeded();
+ sendPowerHintForLaunchEndIfNeeded();
removeSleepTimeouts();
@@ -4204,22 +4257,15 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
// Handle incorrect launch/move to secondary display if needed.
if (isSecondaryDisplayPreferred) {
- final boolean launchOnSecondaryDisplayFailed;
final int actualDisplayId = task.getStack().mDisplayId;
if (!task.canBeLaunchedOnDisplay(actualDisplayId)) {
- // The task landed on an inappropriate display somehow, move it to the default
- // display.
- // TODO(multi-display): Find proper stack for the task on the default display.
- mService.setTaskWindowingMode(task.taskId,
- WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY, true /* toTop */);
- launchOnSecondaryDisplayFailed = true;
- } else {
- // The task might have landed on a display different from requested.
- launchOnSecondaryDisplayFailed = actualDisplayId == DEFAULT_DISPLAY
- || (preferredDisplayId != INVALID_DISPLAY
- && preferredDisplayId != actualDisplayId);
+ throw new IllegalStateException("Task resolved to incompatible display");
}
- if (launchOnSecondaryDisplayFailed) {
+ // The task might have landed on a display different from requested.
+ // TODO(multi-display): Find proper stack for the task on the default display.
+ mService.setTaskWindowingMode(task.taskId,
+ WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY, true /* toTop */);
+ if (preferredDisplayId != actualDisplayId) {
// Display a warning toast that we tried to put a non-resizeable task on a secondary
// display with config different from global config.
mService.mTaskChangeNotificationController
@@ -4473,7 +4519,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
*
* @param task The task to put into resizing mode
*/
- private void setResizingDuringAnimation(TaskRecord task) {
+ void setResizingDuringAnimation(TaskRecord task) {
mResizingTasksDuringAnimation.add(task.taskId);
task.setTaskDockedResizing(true);
}
@@ -4532,8 +4578,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
&& task.getRootActivity() != null) {
final ActivityRecord targetActivity = task.getTopActivity();
- mService.mActivityStarter.sendPowerHintForLaunchStartIfNeeded(true /* forceSend */,
- targetActivity);
+ sendPowerHintForLaunchStartIfNeeded(true /* forceSend */, targetActivity);
mActivityMetricsLogger.notifyActivityLaunching();
try {
mService.moveTaskToFrontLocked(task.taskId, 0, bOptions,
@@ -4550,8 +4595,9 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
setResizingDuringAnimation(task);
}
- mService.mActivityStarter.postStartActivityProcessing(task.getTopActivity(),
- ActivityManager.START_TASK_TO_FRONT, task.getStack());
+ mService.getActivityStartController().postStartActivityProcessingForLastStarter(
+ task.getTopActivity(), ActivityManager.START_TASK_TO_FRONT,
+ task.getStack());
return ActivityManager.START_TASK_TO_FRONT;
}
callingUid = task.mCallingUid;
@@ -4559,8 +4605,9 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
intent = task.intent;
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY);
userId = task.userId;
- int result = mService.startActivityInPackage(callingUid, callingPackage, intent, null,
- null, null, 0, 0, bOptions, userId, task, "startActivityFromRecents");
+ int result = mService.getActivityStartController().startActivityInPackage(callingUid,
+ callingPackage, intent, null, null, null, 0, 0, bOptions, userId, task,
+ "startActivityFromRecents");
if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
setResizingDuringAnimation(task);
}
diff --git a/com/android/server/am/ActivityStartController.java b/com/android/server/am/ActivityStartController.java
new file mode 100644
index 00000000..aed49e00
--- /dev/null
+++ b/com/android/server/am/ActivityStartController.java
@@ -0,0 +1,421 @@
+/*
+ * 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 com.android.server.am;
+
+import static android.app.ActivityManager.START_SUCCESS;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.am.ActivityManagerService.ALLOW_FULL_ONLY;
+
+import android.app.ActivityOptions;
+import android.app.IApplicationThread;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.FactoryTest;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.am.ActivityStackSupervisor.PendingActivityLaunch;
+import com.android.server.am.ActivityStarter.DefaultFactory;
+import com.android.server.am.ActivityStarter.Factory;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Controller for delegating activity launches.
+ *
+ * This class' main objective is to take external activity start requests and prepare them into
+ * a series of discrete activity launches that can be handled by an {@link ActivityStarter}. It is
+ * also responsible for handling logic that happens around an activity launch, but doesn't
+ * necessarily influence the activity start. Examples include power hint management, processing
+ * through the pending activity list, and recording home activity launches.
+ */
+public class ActivityStartController {
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityStartController" : TAG_AM;
+
+ private static final int DO_PENDING_ACTIVITY_LAUNCHES_MSG = 1;
+
+ private final ActivityManagerService mService;
+ private final ActivityStackSupervisor mSupervisor;
+
+ /** Last home activity record we attempted to start. */
+ private ActivityRecord mLastHomeActivityStartRecord;
+
+ /** Temporary array to capture start activity results */
+ private ActivityRecord[] tmpOutRecord = new ActivityRecord[1];
+
+ /**The result of the last home activity we attempted to start. */
+ private int mLastHomeActivityStartResult;
+
+ /** A list of activities that are waiting to launch. */
+ private final ArrayList<ActivityStackSupervisor.PendingActivityLaunch>
+ mPendingActivityLaunches = new ArrayList<>();
+
+ private final Factory mFactory;
+
+ private final Handler mHandler;
+
+ private final class StartHandler extends Handler {
+ public StartHandler(Looper looper) {
+ super(looper, null, true);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case DO_PENDING_ACTIVITY_LAUNCHES_MSG:
+ synchronized (mService) {
+ doPendingActivityLaunches(true);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * TODO(b/64750076): Capture information necessary for dump and
+ * {@link #postStartActivityProcessingForLastStarter} rather than keeping the entire object
+ * around
+ */
+ private ActivityStarter mLastStarter;
+
+ ActivityStartController(ActivityManagerService service) {
+ this(service, service.mStackSupervisor,
+ new DefaultFactory(service, service.mStackSupervisor,
+ new ActivityStartInterceptor(service, service.mStackSupervisor)));
+ }
+
+ @VisibleForTesting
+ ActivityStartController(ActivityManagerService service, ActivityStackSupervisor supervisor,
+ Factory factory) {
+ mService = service;
+ mSupervisor = supervisor;
+ mHandler = new StartHandler(mService.mHandlerThread.getLooper());
+ mFactory = factory;
+ mFactory.setController(this);
+ }
+
+ /**
+ * @return A starter to configure and execute starting an activity. It is valid until after
+ * {@link ActivityStarter#execute} is invoked. At that point, the starter should be
+ * considered invalid and no longer modified or used.
+ */
+ ActivityStarter obtainStarter(Intent intent, String reason) {
+ return mFactory.obtain().setIntent(intent).setReason(reason);
+ }
+
+ void onExecutionComplete(ActivityStarter starter) {
+ if (mLastStarter == null) {
+ mLastStarter = mFactory.obtain();
+ }
+
+ mLastStarter.set(starter);
+ mFactory.recycle(starter);
+ }
+
+ /**
+ * TODO(b/64750076): usage of this doesn't seem right. We're making decisions based off the
+ * last starter for an arbitrary task record. Re-evaluate whether we can remove.
+ */
+ void postStartActivityProcessingForLastStarter(ActivityRecord r, int result,
+ ActivityStack targetStack) {
+ if (mLastStarter == null) {
+ return;
+ }
+
+ mLastStarter.postStartActivityProcessing(r, result, targetStack);
+ }
+
+ void startHomeActivity(Intent intent, ActivityInfo aInfo, String reason) {
+ mSupervisor.moveHomeStackTaskToTop(reason);
+
+ mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
+ .setOutActivity(tmpOutRecord)
+ .setCallingUid(0)
+ .setActivityInfo(aInfo)
+ .execute();
+ mLastHomeActivityStartRecord = tmpOutRecord[0];
+ if (mSupervisor.inResumeTopActivity) {
+ // If we are in resume section already, home activity will be initialized, but not
+ // resumed (to avoid recursive resume) and will stay that way until something pokes it
+ // again. We need to schedule another resume.
+ mSupervisor.scheduleResumeTopActivities();
+ }
+ }
+
+ /**
+ * Starts the "new version setup screen" if appropriate.
+ */
+ void startSetupActivity() {
+ // Only do this once per boot.
+ if (mService.getCheckedForSetup()) {
+ return;
+ }
+
+ // We will show this screen if the current one is a different
+ // version than the last one shown, and we are not running in
+ // low-level factory test mode.
+ final ContentResolver resolver = mService.mContext.getContentResolver();
+ if (mService.mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL &&
+ Settings.Global.getInt(resolver,
+ Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
+ mService.setCheckedForSetup(true);
+
+ // See if we should be showing the platform update setup UI.
+ final Intent intent = new Intent(Intent.ACTION_UPGRADE_SETUP);
+ final List<ResolveInfo> ris = mService.mContext.getPackageManager()
+ .queryIntentActivities(intent,
+ PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA);
+ if (!ris.isEmpty()) {
+ final ResolveInfo ri = ris.get(0);
+ String vers = ri.activityInfo.metaData != null
+ ? ri.activityInfo.metaData.getString(Intent.METADATA_SETUP_VERSION)
+ : null;
+ if (vers == null && ri.activityInfo.applicationInfo.metaData != null) {
+ vers = ri.activityInfo.applicationInfo.metaData.getString(
+ Intent.METADATA_SETUP_VERSION);
+ }
+ String lastVers = Settings.Secure.getString(
+ resolver, Settings.Secure.LAST_SETUP_SHOWN);
+ if (vers != null && !vers.equals(lastVers)) {
+ intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
+ intent.setComponent(new ComponentName(
+ ri.activityInfo.packageName, ri.activityInfo.name));
+ obtainStarter(intent, "startSetupActivity")
+ .setCallingUid(0)
+ .setActivityInfo(ri.activityInfo)
+ .execute();
+ }
+ }
+ }
+ }
+
+ final int startActivityInPackage(int uid, String callingPackage,
+ Intent intent, String resolvedType, IBinder resultTo,
+ String resultWho, int requestCode, int startFlags, Bundle bOptions, int userId,
+ TaskRecord inTask, String reason) {
+
+ userId = mService.mUserController.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, false, ALLOW_FULL_ONLY, "startActivityInPackage",
+ null);
+
+ // TODO: Switch to user app stacks here.
+ return obtainStarter(intent, reason)
+ .setCallingUid(uid)
+ .setCallingPackage(callingPackage)
+ .setResolvedType(resolvedType)
+ .setResultTo(resultTo)
+ .setResultWho(resultWho)
+ .setRequestCode(requestCode)
+ .setStartFlags(startFlags)
+ .setMayWait(bOptions, userId)
+ .setInTask(inTask)
+ .execute();
+ }
+
+ final int startActivitiesInPackage(int uid, String callingPackage, Intent[] intents,
+ String[] resolvedTypes, IBinder resultTo, Bundle bOptions, int userId) {
+ final String reason = "startActivityInPackage";
+ userId = mService.mUserController.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, false, ALLOW_FULL_ONLY, reason, null);
+ // TODO: Switch to user app stacks here.
+ int ret = startActivities(null, uid, callingPackage, intents, resolvedTypes, resultTo,
+ bOptions, userId, reason);
+ return ret;
+ }
+
+ int startActivities(IApplicationThread caller, int callingUid, String callingPackage,
+ Intent[] intents, String[] resolvedTypes, IBinder resultTo, Bundle bOptions, int userId,
+ String reason) {
+ if (intents == null) {
+ throw new NullPointerException("intents is null");
+ }
+ if (resolvedTypes == null) {
+ throw new NullPointerException("resolvedTypes is null");
+ }
+ if (intents.length != resolvedTypes.length) {
+ throw new IllegalArgumentException("intents are length different than resolvedTypes");
+ }
+
+ final int realCallingPid = Binder.getCallingPid();
+ final int realCallingUid = Binder.getCallingUid();
+
+ int callingPid;
+ if (callingUid >= 0) {
+ callingPid = -1;
+ } else if (caller == null) {
+ callingPid = realCallingPid;
+ callingUid = realCallingUid;
+ } else {
+ callingPid = callingUid = -1;
+ }
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ synchronized (mService) {
+ ActivityRecord[] outActivity = new ActivityRecord[1];
+ for (int i=0; i < intents.length; i++) {
+ Intent intent = intents[i];
+ if (intent == null) {
+ continue;
+ }
+
+ // Refuse possible leaked file descriptors
+ if (intent != null && intent.hasFileDescriptors()) {
+ throw new IllegalArgumentException("File descriptors passed in Intent");
+ }
+
+ boolean componentSpecified = intent.getComponent() != null;
+
+ // Don't modify the client's object!
+ intent = new Intent(intent);
+
+ // Collect information about the target of the Intent.
+ ActivityInfo aInfo = mSupervisor.resolveActivity(intent, resolvedTypes[i], 0,
+ null, userId);
+ // TODO: New, check if this is correct
+ aInfo = mService.getActivityInfoForUser(aInfo, userId);
+
+ if (aInfo != null &&
+ (aInfo.applicationInfo.privateFlags
+ & ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) {
+ throw new IllegalArgumentException(
+ "FLAG_CANT_SAVE_STATE not supported here");
+ }
+
+ ActivityOptions options = ActivityOptions.fromBundle(
+ i == intents.length - 1 ? bOptions : null);
+
+ final int res = obtainStarter(intent, reason)
+ .setCaller(caller)
+ .setResolvedType(resolvedTypes[i])
+ .setActivityInfo(aInfo)
+ .setResultTo(resultTo)
+ .setRequestCode(-1)
+ .setCallingPid(callingPid)
+ .setCallingUid(callingUid)
+ .setCallingPackage(callingPackage)
+ .setRealCallingPid(realCallingPid)
+ .setRealCallingUid(realCallingUid)
+ .setActivityOptions(options)
+ .setComponentSpecified(componentSpecified)
+ .setOutActivity(outActivity)
+ .execute();
+
+ if (res < 0) {
+ return res;
+ }
+
+ resultTo = outActivity[0] != null ? outActivity[0].appToken : null;
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+
+ return START_SUCCESS;
+ }
+
+ void schedulePendingActivityLaunches(long delayMs) {
+ mHandler.removeMessages(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
+ Message msg = mHandler.obtainMessage(DO_PENDING_ACTIVITY_LAUNCHES_MSG);
+ mHandler.sendMessageDelayed(msg, delayMs);
+ }
+
+ void doPendingActivityLaunches(boolean doResume) {
+ while (!mPendingActivityLaunches.isEmpty()) {
+ final PendingActivityLaunch pal = mPendingActivityLaunches.remove(0);
+ final boolean resume = doResume && mPendingActivityLaunches.isEmpty();
+ final ActivityStarter starter = obtainStarter(null /* intent */,
+ "pendingActivityLaunch");
+ try {
+ starter.startResolvedActivity(pal.r, pal.sourceRecord, null, null, pal.startFlags,
+ resume, null, null, null /* outRecords */);
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception during pending activity launch pal=" + pal, e);
+ pal.sendErrorResult(e.getMessage());
+ }
+ }
+ }
+
+ void addPendingActivityLaunch(PendingActivityLaunch launch) {
+ mPendingActivityLaunches.add(launch);
+ }
+
+ boolean clearPendingActivityLaunches(String packageName) {
+ final int pendingLaunches = mPendingActivityLaunches.size();
+
+ for (int palNdx = pendingLaunches - 1; palNdx >= 0; --palNdx) {
+ final PendingActivityLaunch pal = mPendingActivityLaunches.get(palNdx);
+ final ActivityRecord r = pal.r;
+ if (r != null && r.packageName.equals(packageName)) {
+ mPendingActivityLaunches.remove(palNdx);
+ }
+ }
+ return mPendingActivityLaunches.size() < pendingLaunches;
+ }
+
+ void dump(PrintWriter pw, String prefix, String dumpPackage) {
+ pw.print(prefix);
+ pw.print("mLastHomeActivityStartResult=");
+ pw.println(mLastHomeActivityStartResult);
+
+ if (mLastHomeActivityStartRecord != null) {
+ pw.print(prefix);
+ pw.println("mLastHomeActivityStartRecord:");
+ mLastHomeActivityStartRecord.dump(pw, prefix + " ");
+ }
+
+ final boolean dumpPackagePresent = dumpPackage != null;
+
+ if (mLastStarter != null) {
+ final boolean dump = !dumpPackagePresent
+ || mLastStarter.relatedToPackage(dumpPackage)
+ || (mLastHomeActivityStartRecord != null
+ && dumpPackage.equals(mLastHomeActivityStartRecord.packageName));
+
+ if (dump) {
+ pw.print(prefix);
+ mLastStarter.dump(pw, prefix + " ");
+
+ if (dumpPackagePresent) {
+ return;
+ }
+ }
+ }
+
+ if (dumpPackagePresent) {
+ pw.print(prefix);
+ pw.println("(nothing)");
+ }
+ }
+}
diff --git a/com/android/server/am/ActivityStarter.java b/com/android/server/am/ActivityStarter.java
index 2fc5dda6..abdbfadf 100644
--- a/com/android/server/am/ActivityStarter.java
+++ b/com/android/server/am/ActivityStarter.java
@@ -33,7 +33,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECOND
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
-import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -45,7 +44,6 @@ import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
import static android.content.Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
-import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK;
@@ -55,7 +53,6 @@ import static android.view.Display.INVALID_DISPLAY;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_CONFIGURATION;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOCUS;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_RECENTS;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_RESULTS;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_STACK;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_TASKS;
@@ -79,7 +76,6 @@ import static com.android.server.am.TaskRecord.REPARENT_MOVE_STACK_TO_FRONT;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityOptions;
-import android.app.AppGlobals;
import android.app.IApplicationThread;
import android.app.PendingIntent;
import android.app.ProfilerInfo;
@@ -90,25 +86,26 @@ import android.content.IntentSender;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.AuxiliaryResolveInfo;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
-import android.hardware.power.V1_0.PowerHint;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.service.voice.IVoiceInteractionSession;
import android.text.TextUtils;
import android.util.EventLog;
+import android.util.Pools.SynchronizedPool;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.HeavyWeightSwitcherActivity;
import com.android.internal.app.IVoiceInteractor;
import com.android.server.am.ActivityStackSupervisor.PendingActivityLaunch;
@@ -116,11 +113,10 @@ import com.android.server.pm.InstantAppResolver;
import java.io.PrintWriter;
import java.text.DateFormat;
-import java.util.ArrayList;
import java.util.Date;
/**
- * Controller for interpreting how and then launching activities.
+ * Controller for interpreting how and then launching an activity.
*
* This class collects all the logic for determining how an intent and flags should be turned into
* an activity and associated task and stack.
@@ -136,8 +132,7 @@ class ActivityStarter {
private final ActivityManagerService mService;
private final ActivityStackSupervisor mSupervisor;
private final ActivityStartInterceptor mInterceptor;
-
- final ArrayList<PendingActivityLaunch> mPendingActivityLaunches = new ArrayList<>();
+ private final ActivityStartController mController;
// Share state variable among methods when starting an activity.
private ActivityRecord mStartActivity;
@@ -171,7 +166,6 @@ class ActivityStarter {
private boolean mNoAnimation;
private boolean mKeepCurTransition;
private boolean mAvoidMoveToFront;
- private boolean mPowerHintSent;
// We must track when we deliver the new intent since multiple code paths invoke
// {@link #deliverNewIntent}. This is due to early returns in the code path. This flag is used
@@ -182,10 +176,6 @@ class ActivityStarter {
private IVoiceInteractionSession mVoiceSession;
private IVoiceInteractor mVoiceInteractor;
- // Last home activity record we attempted to start
- private final ActivityRecord[] mLastHomeActivityStartRecord = new ActivityRecord[1];
- // The result of the last home activity we attempted to start.
- private int mLastHomeActivityStartResult;
// Last activity record we attempted to start
private final ActivityRecord[] mLastStartActivityRecord = new ActivityRecord[1];
// The result of the last activity we attempted to start.
@@ -195,51 +185,323 @@ class ActivityStarter {
// The reason we were trying to start the last activity
private String mLastStartReason;
- private void reset() {
- mStartActivity = null;
- mIntent = null;
- mCallingUid = -1;
- mOptions = null;
+ /*
+ * Request details provided through setter methods. Should be reset after {@link #execute()}
+ * to avoid unnecessarily retaining parameters. Note that the request is ignored when
+ * {@link #startResolvedActivity} is invoked directly.
+ */
+ private Request mRequest = new Request();
- mLaunchTaskBehind = false;
- mLaunchFlags = 0;
- mLaunchMode = INVALID_LAUNCH_MODE;
+ /**
+ * An interface that to provide {@link ActivityStarter} instances to the controller. This is
+ * used by tests to inject their own starter implementations for verification purposes.
+ */
+ @VisibleForTesting
+ interface Factory {
+ /**
+ * Sets the {@link ActivityStartController} to be passed to {@link ActivityStarter}.
+ */
+ void setController(ActivityStartController controller);
+
+ /**
+ * Generates an {@link ActivityStarter} that is ready to handle a new start request.
+ * @param controller The {@link ActivityStartController} which the starter who will own
+ * this instance.
+ * @return an {@link ActivityStarter}
+ */
+ ActivityStarter obtain();
+
+ /**
+ * Recycles a starter for reuse.
+ */
+ void recycle(ActivityStarter starter);
+ }
- mLaunchBounds.setEmpty();
+ /**
+ * Default implementation of {@link StarterFactory}.
+ */
+ static class DefaultFactory implements Factory {
+ /**
+ * The maximum count of starters that should be active at one time:
+ * 1. last ran starter (for logging and post activity processing)
+ * 2. current running starter
+ * 3. starter from re-entry in (2)
+ */
+ private final int MAX_STARTER_COUNT = 3;
- mNotTop = null;
- mDoResume = false;
- mStartFlags = 0;
- mSourceRecord = null;
- mPreferredDisplayId = INVALID_DISPLAY;
+ private ActivityStartController mController;
+ private ActivityManagerService mService;
+ private ActivityStackSupervisor mSupervisor;
+ private ActivityStartInterceptor mInterceptor;
- mInTask = null;
- mAddingToTask = false;
- mReuseTask = null;
+ private SynchronizedPool<ActivityStarter> mStarterPool =
+ new SynchronizedPool<>(MAX_STARTER_COUNT);
- mNewTaskInfo = null;
- mNewTaskIntent = null;
- mSourceStack = null;
+ DefaultFactory(ActivityManagerService service,
+ ActivityStackSupervisor supervisor, ActivityStartInterceptor interceptor) {
+ mService = service;
+ mSupervisor = supervisor;
+ mInterceptor = interceptor;
+ }
- mTargetStack = null;
- mMovedToFront = false;
- mNoAnimation = false;
- mKeepCurTransition = false;
- mAvoidMoveToFront = false;
+ @Override
+ public void setController(ActivityStartController controller) {
+ mController = controller;
+ }
- mVoiceSession = null;
- mVoiceInteractor = null;
+ @Override
+ public ActivityStarter obtain() {
+ ActivityStarter starter = mStarterPool.acquire();
- mIntentDelivered = false;
+ if (starter == null) {
+ starter = new ActivityStarter(mController, mService, mSupervisor, mInterceptor);
+ }
+
+ return starter;
+ }
+
+ @Override
+ public void recycle(ActivityStarter starter) {
+ starter.reset(true /* clearRequest*/);
+ mStarterPool.release(starter);
+ }
+ }
+
+ /**
+ * Container for capturing initial start request details. This information is NOT reset until
+ * the {@link ActivityStarter} is recycled, allowing for multiple invocations with the same
+ * parameters.
+ *
+ * TODO(b/64750076): Investigate consolidating member variables of {@link ActivityStarter} with
+ * the request object. Note that some member variables are referenced in
+ * {@link #dump(PrintWriter, String)} and therefore cannot be cleared immediately after
+ * execution.
+ */
+ private static class Request {
+ private static final int DEFAULT_CALLING_UID = -1;
+ private static final int DEFAULT_CALLING_PID = 0;
+
+ IApplicationThread caller;
+ Intent intent;
+ Intent ephemeralIntent;
+ String resolvedType;
+ ActivityInfo activityInfo;
+ ResolveInfo resolveInfo;
+ IVoiceInteractionSession voiceSession;
+ IVoiceInteractor voiceInteractor;
+ IBinder resultTo;
+ String resultWho;
+ int requestCode;
+ int callingPid = DEFAULT_CALLING_UID;
+ int callingUid = DEFAULT_CALLING_PID;
+ String callingPackage;
+ int realCallingPid;
+ int realCallingUid;
+ int startFlags;
+ ActivityOptions activityOptions;
+ boolean ignoreTargetSecurity;
+ boolean componentSpecified;
+ ActivityRecord[] outActivity;
+ TaskRecord inTask;
+ String reason;
+ ProfilerInfo profilerInfo;
+ Configuration globalConfig;
+ Bundle waitOptions;
+ int userId;
+ WaitResult waitResult;
+
+ /**
+ * Indicates that we should wait for the result of the start request. This flag is set when
+ * {@link ActivityStarter#setMayWait(Bundle, int)} is called.
+ * {@see ActivityStarter#startActivityMayWait}.
+ */
+ boolean mayWait;
+
+ /**
+ * Sets values back to the initial state, clearing any held references.
+ */
+ void reset() {
+ caller = null;
+ intent = null;
+ ephemeralIntent = null;
+ resolvedType = null;
+ activityInfo = null;
+ resolveInfo = null;
+ voiceSession = null;
+ voiceInteractor = null;
+ resultTo = null;
+ resultWho = null;
+ requestCode = 0;
+ callingPid = 0;
+ callingUid = 0;
+ callingPackage = null;
+ realCallingPid = 0;
+ realCallingUid = 0;
+ startFlags = 0;
+ activityOptions = null;
+ ignoreTargetSecurity = false;
+ componentSpecified = false;
+ outActivity = null;
+ inTask = null;
+ reason = null;
+ profilerInfo = null;
+ globalConfig = null;
+ waitOptions = null;
+ userId = 0;
+ waitResult = null;
+ mayWait = false;
+ }
+
+ /**
+ * Adopts all values from passed in request.
+ */
+ void set(Request request) {
+ caller = request.caller;
+ intent = request.intent;
+ ephemeralIntent = request.ephemeralIntent;
+ resolvedType = request.resolvedType;
+ activityInfo = request.activityInfo;
+ resolveInfo = request.resolveInfo;
+ voiceSession = request.voiceSession;
+ voiceInteractor = request.voiceInteractor;
+ resultTo = request.resultTo;
+ resultWho = request.resultWho;
+ requestCode = request.requestCode;
+ callingPid = request.callingPid;
+ callingUid = request.callingUid;
+ callingPackage = request.callingPackage;
+ realCallingPid = request.realCallingPid;
+ realCallingUid = request.realCallingUid;
+ startFlags = request.startFlags;
+ activityOptions = request.activityOptions;
+ ignoreTargetSecurity = request.ignoreTargetSecurity;
+ componentSpecified = request.componentSpecified;
+ outActivity = request.outActivity;
+ inTask = request.inTask;
+ reason = request.reason;
+ profilerInfo = request.profilerInfo;
+ globalConfig = request.globalConfig;
+ waitOptions = request.waitOptions;
+ userId = request.userId;
+ waitResult = request.waitResult;
+ mayWait = request.mayWait;
+ }
}
- ActivityStarter(ActivityManagerService service) {
+ ActivityStarter(ActivityStartController controller, ActivityManagerService service,
+ ActivityStackSupervisor supervisor, ActivityStartInterceptor interceptor) {
+ mController = controller;
mService = service;
- mSupervisor = mService.mStackSupervisor;
- mInterceptor = new ActivityStartInterceptor(mService, mSupervisor);
+ mSupervisor = supervisor;
+ mInterceptor = interceptor;
+ reset(true);
+ }
+
+ /**
+ * Effectively duplicates the starter passed in. All state and request values will be
+ * mirrored.
+ * @param starter
+ */
+ void set(ActivityStarter starter) {
+ mStartActivity = starter.mStartActivity;
+ mIntent = starter.mIntent;
+ mCallingUid = starter.mCallingUid;
+ mOptions = starter.mOptions;
+
+ mLaunchTaskBehind = starter.mLaunchTaskBehind;
+ mLaunchFlags = starter.mLaunchFlags;
+ mLaunchMode = starter.mLaunchMode;
+
+ mLaunchBounds.set(starter.mLaunchBounds);
+
+ mNotTop = starter.mNotTop;
+ mDoResume = starter.mDoResume;
+ mStartFlags = starter.mStartFlags;
+ mSourceRecord = starter.mSourceRecord;
+ mPreferredDisplayId = starter.mPreferredDisplayId;
+
+ mInTask = starter.mInTask;
+ mAddingToTask = starter.mAddingToTask;
+ mReuseTask = starter.mReuseTask;
+
+ mNewTaskInfo = starter.mNewTaskInfo;
+ mNewTaskIntent = starter.mNewTaskIntent;
+ mSourceStack = starter.mSourceStack;
+
+ mTargetStack = starter.mTargetStack;
+ mMovedToFront = starter.mMovedToFront;
+ mNoAnimation = starter.mNoAnimation;
+ mKeepCurTransition = starter.mKeepCurTransition;
+ mAvoidMoveToFront = starter.mAvoidMoveToFront;
+
+ mVoiceSession = starter.mVoiceSession;
+ mVoiceInteractor = starter.mVoiceInteractor;
+
+ mIntentDelivered = starter.mIntentDelivered;
+
+ mRequest.set(starter.mRequest);
+ }
+
+ ActivityRecord getStartActivity() {
+ return mStartActivity;
}
- int startActivityLocked(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
+ boolean relatedToPackage(String packageName) {
+ return (mLastStartActivityRecord[0] != null
+ && packageName.equals(mLastStartActivityRecord[0].packageName))
+ || (mStartActivity != null && packageName.equals(mStartActivity.packageName));
+ }
+
+ /**
+ * Starts an activity based on the request parameters provided earlier.
+ * @return The starter result.
+ */
+ int execute() {
+ try {
+ // TODO(b/64750076): Look into passing request directly to these methods to allow
+ // for transactional diffs and preprocessing.
+ if (mRequest.mayWait) {
+ return startActivityMayWait(mRequest.caller, mRequest.callingUid,
+ mRequest.callingPackage, mRequest.intent, mRequest.resolvedType,
+ mRequest.voiceSession, mRequest.voiceInteractor, mRequest.resultTo,
+ mRequest.resultWho, mRequest.requestCode, mRequest.startFlags,
+ mRequest.profilerInfo, mRequest.waitResult, mRequest.globalConfig,
+ mRequest.waitOptions, mRequest.ignoreTargetSecurity, mRequest.userId,
+ mRequest.inTask, mRequest.reason);
+ } else {
+ return startActivity(mRequest.caller, mRequest.intent, mRequest.ephemeralIntent,
+ mRequest.resolvedType, mRequest.activityInfo, mRequest.resolveInfo,
+ mRequest.voiceSession, mRequest.voiceInteractor, mRequest.resultTo,
+ mRequest.resultWho, mRequest.requestCode, mRequest.callingPid,
+ mRequest.callingUid, mRequest.callingPackage, mRequest.realCallingPid,
+ mRequest.realCallingUid, mRequest.startFlags, mRequest.activityOptions,
+ mRequest.ignoreTargetSecurity, mRequest.componentSpecified,
+ mRequest.outActivity, mRequest.inTask, mRequest.reason);
+ }
+ } finally {
+ onExecutionComplete();
+ }
+ }
+
+ /**
+ * Starts an activity based on the provided {@link ActivityRecord} and environment parameters.
+ * Note that this method is called internally as well as part of {@link #startActivity}.
+ *
+ * @return The start result.
+ */
+ int startResolvedActivity(final ActivityRecord r, ActivityRecord sourceRecord,
+ IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
+ int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
+ ActivityRecord[] outActivity) {
+ try {
+ return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags,
+ doResume, options, inTask, outActivity);
+ } finally {
+ onExecutionComplete();
+ }
+ }
+
+ private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
@@ -268,12 +530,19 @@ class ActivityStarter {
return getExternalResult(mLastStartActivityResult);
}
- public static int getExternalResult(int result) {
+ static int getExternalResult(int result) {
// Aborted results are treated as successes externally, but we must track them internally.
return result != START_ABORTED ? result : START_SUCCESS;
}
- /** DO NOT call this method directly. Use {@link #startActivityLocked} instead. */
+ /**
+ * Called when execution is complete. Sets state indicating completion and proceeds with
+ * recycling if appropriate.
+ */
+ private void onExecutionComplete() {
+ mController.onExecutionComplete(this);
+ }
+
private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
@@ -536,9 +805,8 @@ class ActivityStarter {
|| stack.mResumedActivity.info.applicationInfo.uid != callingUid)) {
if (!mService.checkAppSwitchAllowedLocked(callingPid, callingUid,
realCallingPid, realCallingUid, "Activity start")) {
- PendingActivityLaunch pal = new PendingActivityLaunch(r,
- sourceRecord, startFlags, stack, callerApp);
- mPendingActivityLaunches.add(pal);
+ mController.addPendingActivityLaunch(new PendingActivityLaunch(r,
+ sourceRecord, startFlags, stack, callerApp));
ActivityOptions.abort(options);
return ActivityManager.START_SWITCHES_CANCELED;
}
@@ -555,10 +823,10 @@ class ActivityStarter {
mService.mDidAppSwitch = true;
}
- doPendingActivityLaunchesLocked(false);
+ mController.doPendingActivityLaunches(false);
- return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, true,
- options, inTask, outActivity);
+ return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags,
+ true /* doResume */, options, inTask, outActivity);
}
/**
@@ -578,11 +846,11 @@ class ActivityStarter {
auxiliaryResponse.failureIntent, callingPackage, verificationBundle,
resolvedType, userId, auxiliaryResponse.packageName, auxiliaryResponse.splitName,
auxiliaryResponse.installFailureActivity, auxiliaryResponse.versionCode,
- auxiliaryResponse.token, auxiliaryResponse.needsPhaseTwo);
+ auxiliaryResponse.token, auxiliaryResponse.resolveInfo.getExtras(),
+ auxiliaryResponse.needsPhaseTwo);
}
void postStartActivityProcessing(ActivityRecord r, int result, ActivityStack targetStack) {
-
if (ActivityManager.isStartResultFatalError(result)) {
return;
}
@@ -620,35 +888,7 @@ class ActivityStarter {
}
}
- void startHomeActivityLocked(Intent intent, ActivityInfo aInfo, String reason) {
- mSupervisor.moveHomeStackTaskToTop(reason);
- mLastHomeActivityStartResult = startActivityLocked(null /*caller*/, intent,
- null /*ephemeralIntent*/, null /*resolvedType*/, aInfo, null /*rInfo*/,
- null /*voiceSession*/, null /*voiceInteractor*/, null /*resultTo*/,
- null /*resultWho*/, 0 /*requestCode*/, 0 /*callingPid*/, 0 /*callingUid*/,
- null /*callingPackage*/, 0 /*realCallingPid*/, 0 /*realCallingUid*/,
- 0 /*startFlags*/, null /*options*/, false /*ignoreTargetSecurity*/,
- false /*componentSpecified*/, mLastHomeActivityStartRecord /*outActivity*/,
- null /*inTask*/, "startHomeActivity: " + reason);
- if (mSupervisor.inResumeTopActivity) {
- // If we are in resume section already, home activity will be initialized, but not
- // resumed (to avoid recursive resume) and will stay that way until something pokes it
- // again. We need to schedule another resume.
- mSupervisor.scheduleResumeTopActivities();
- }
- }
-
- void startConfirmCredentialIntent(Intent intent, Bundle optionsBundle) {
- intent.addFlags(FLAG_ACTIVITY_NEW_TASK |
- FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS |
- FLAG_ACTIVITY_TASK_ON_HOME);
- ActivityOptions options = (optionsBundle != null ? new ActivityOptions(optionsBundle)
- : ActivityOptions.makeBasic());
- options.setLaunchTaskId(mSupervisor.getHomeActivity().getTask().taskId);
- mService.mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT);
- }
-
- final int startActivityMayWait(IApplicationThread caller, int callingUid,
+ private int startActivityMayWait(IApplicationThread caller, int callingUid,
String callingPackage, Intent intent, String resolvedType,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode, int startFlags,
@@ -792,7 +1032,7 @@ class ActivityStarter {
}
final ActivityRecord[] outRecord = new ActivityRecord[1];
- int res = startActivityLocked(caller, intent, ephemeralIntent, resolvedType,
+ int res = startActivity(caller, intent, ephemeralIntent, resolvedType,
aInfo, rInfo, voiceSession, voiceInteractor,
resultTo, resultWho, requestCode, callingPid,
callingUid, callingPackage, realCallingPid, realCallingUid, startFlags,
@@ -858,115 +1098,10 @@ class ActivityStarter {
}
}
- final int startActivities(IApplicationThread caller, int callingUid, String callingPackage,
- Intent[] intents, String[] resolvedTypes, IBinder resultTo,
- Bundle bOptions, int userId, String reason) {
- if (intents == null) {
- throw new NullPointerException("intents is null");
- }
- if (resolvedTypes == null) {
- throw new NullPointerException("resolvedTypes is null");
- }
- if (intents.length != resolvedTypes.length) {
- throw new IllegalArgumentException("intents are length different than resolvedTypes");
- }
-
- final int realCallingPid = Binder.getCallingPid();
- final int realCallingUid = Binder.getCallingUid();
-
- int callingPid;
- if (callingUid >= 0) {
- callingPid = -1;
- } else if (caller == null) {
- callingPid = realCallingPid;
- callingUid = realCallingUid;
- } else {
- callingPid = callingUid = -1;
- }
- final long origId = Binder.clearCallingIdentity();
- try {
- synchronized (mService) {
- ActivityRecord[] outActivity = new ActivityRecord[1];
- for (int i=0; i<intents.length; i++) {
- Intent intent = intents[i];
- if (intent == null) {
- continue;
- }
-
- // Refuse possible leaked file descriptors
- if (intent != null && intent.hasFileDescriptors()) {
- throw new IllegalArgumentException("File descriptors passed in Intent");
- }
-
- boolean componentSpecified = intent.getComponent() != null;
-
- // Don't modify the client's object!
- intent = new Intent(intent);
-
- // Collect information about the target of the Intent.
- ActivityInfo aInfo = mSupervisor.resolveActivity(intent, resolvedTypes[i], 0,
- null, userId);
- // TODO: New, check if this is correct
- aInfo = mService.getActivityInfoForUser(aInfo, userId);
-
- if (aInfo != null &&
- (aInfo.applicationInfo.privateFlags
- & ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) {
- throw new IllegalArgumentException(
- "FLAG_CANT_SAVE_STATE not supported here");
- }
-
- ActivityOptions options = ActivityOptions.fromBundle(
- i == intents.length - 1 ? bOptions : null);
- int res = startActivityLocked(caller, intent, null /*ephemeralIntent*/,
- resolvedTypes[i], aInfo, null /*rInfo*/, null, null, resultTo, null, -1,
- callingPid, callingUid, callingPackage,
- realCallingPid, realCallingUid, 0,
- options, false, componentSpecified, outActivity, null, reason);
- if (res < 0) {
- return res;
- }
-
- resultTo = outActivity[0] != null ? outActivity[0].appToken : null;
- }
- }
- } finally {
- Binder.restoreCallingIdentity(origId);
- }
-
- return START_SUCCESS;
- }
-
- void sendPowerHintForLaunchStartIfNeeded(boolean forceSend, ActivityRecord targetActivity) {
- boolean sendHint = forceSend;
-
- if (!sendHint) {
- // If not forced, send power hint when the activity's process is different than the
- // current resumed activity.
- final ActivityRecord resumedActivity = mSupervisor.getResumedActivityLocked();
- sendHint = resumedActivity == null
- || resumedActivity.app == null
- || !resumedActivity.app.equals(targetActivity.app);
- }
-
- if (sendHint && mService.mLocalPowerManager != null) {
- mService.mLocalPowerManager.powerHint(PowerHint.LAUNCH, 1);
- mPowerHintSent = true;
- }
- }
-
- void sendPowerHintForLaunchEndIfNeeded() {
- // Trigger launch power hint if activity is launched
- if (mPowerHintSent && mService.mLocalPowerManager != null) {
- mService.mLocalPowerManager.powerHint(PowerHint.LAUNCH, 0);
- mPowerHintSent = false;
- }
- }
-
private int startActivity(final ActivityRecord r, ActivityRecord sourceRecord,
- IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
- int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
- ActivityRecord[] outActivity) {
+ IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
+ int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
+ ActivityRecord[] outActivity) {
int result = START_CANCELED;
try {
mService.mWindowManager.deferSurfaceLayout();
@@ -1064,7 +1199,7 @@ class ActivityStarter {
}
}
- sendPowerHintForLaunchStartIfNeeded(false /* forceSend */, reusedActivity);
+ mSupervisor.sendPowerHintForLaunchStartIfNeeded(false /* forceSend */, reusedActivity);
reusedActivity = setTargetStackAndMoveToFrontIfNeeded(reusedActivity);
@@ -1179,7 +1314,7 @@ class ActivityStarter {
EventLogTags.AM_CREATE_ACTIVITY, mStartActivity, mStartActivity.getTask());
mTargetStack.mLastPausedActivity = null;
- sendPowerHintForLaunchStartIfNeeded(false /* forceSend */, mStartActivity);
+ mSupervisor.sendPowerHintForLaunchStartIfNeeded(false /* forceSend */, mStartActivity);
mTargetStack.startActivityLocked(mStartActivity, topFocused, newTask, mKeepCurTransition,
mOptions);
@@ -1222,10 +1357,56 @@ class ActivityStarter {
return START_SUCCESS;
}
+ /**
+ * Resets the {@link ActivityStarter} state.
+ * @param clearRequest whether the request should be reset to default values.
+ */
+ void reset(boolean clearRequest) {
+ mStartActivity = null;
+ mIntent = null;
+ mCallingUid = -1;
+ mOptions = null;
+
+ mLaunchTaskBehind = false;
+ mLaunchFlags = 0;
+ mLaunchMode = INVALID_LAUNCH_MODE;
+
+ mLaunchBounds.setEmpty();
+
+ mNotTop = null;
+ mDoResume = false;
+ mStartFlags = 0;
+ mSourceRecord = null;
+ mPreferredDisplayId = INVALID_DISPLAY;
+
+ mInTask = null;
+ mAddingToTask = false;
+ mReuseTask = null;
+
+ mNewTaskInfo = null;
+ mNewTaskIntent = null;
+ mSourceStack = null;
+
+ mTargetStack = null;
+ mMovedToFront = false;
+ mNoAnimation = false;
+ mKeepCurTransition = false;
+ mAvoidMoveToFront = false;
+
+ mVoiceSession = null;
+ mVoiceInteractor = null;
+
+ mIntentDelivered = false;
+
+ if (clearRequest) {
+ mRequest.reset();
+ }
+ }
+
private void setInitialState(ActivityRecord r, ActivityOptions options, TaskRecord inTask,
boolean doResume, int startFlags, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor) {
- reset();
+ reset(false /* clearRequest */);
mStartActivity = r;
mIntent = r.intent;
@@ -1631,6 +1812,9 @@ class ActivityStarter {
}
}
}
+ // Need to update mTargetStack because if task was moved out of it, the original stack may
+ // be destroyed.
+ mTargetStack = intentActivity.getStack();
if (!mMovedToFront && mDoResume) {
if (DEBUG_TASKS) Slog.d(TAG_TASKS, "Bring to front target: " + mTargetStack
+ " from " + intentActivity);
@@ -1933,6 +2117,7 @@ class ActivityStarter {
return START_SUCCESS;
}
+ @VisibleForTesting
void updateBounds(TaskRecord task, Rect bounds) {
if (bounds.isEmpty()) {
return;
@@ -1996,20 +2181,6 @@ class ActivityStarter {
return launchFlags;
}
- final void doPendingActivityLaunchesLocked(boolean doResume) {
- while (!mPendingActivityLaunches.isEmpty()) {
- final PendingActivityLaunch pal = mPendingActivityLaunches.remove(0);
- final boolean resume = doResume && mPendingActivityLaunches.isEmpty();
- try {
- startActivity(pal.r, pal.sourceRecord, null, null, pal.startFlags, resume, null,
- null, null /*outRecords*/);
- } catch (Exception e) {
- Slog.e(TAG, "Exception during pending activity launch pal=" + pal, e);
- pal.sendErrorResult(e.getMessage());
- }
- }
- }
-
private ActivityStack computeStackFocus(ActivityRecord r, boolean newTask, int launchFlags,
ActivityOptions aOptions) {
final TaskRecord task = r.getTask();
@@ -2166,35 +2337,157 @@ class ActivityStarter {
(flags & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) == 0;
}
- boolean clearPendingActivityLaunchesLocked(String packageName) {
- boolean didSomething = false;
+ ActivityStarter setIntent(Intent intent) {
+ mRequest.intent = intent;
+ return this;
+ }
- for (int palNdx = mPendingActivityLaunches.size() - 1; palNdx >= 0; --palNdx) {
- PendingActivityLaunch pal = mPendingActivityLaunches.get(palNdx);
- ActivityRecord r = pal.r;
- if (r != null && r.packageName.equals(packageName)) {
- mPendingActivityLaunches.remove(palNdx);
- didSomething = true;
- }
- }
- return didSomething;
+ ActivityStarter setReason(String reason) {
+ mRequest.reason = reason;
+ return this;
}
- void dump(PrintWriter pw, String prefix, String dumpPackage) {
- prefix = prefix + " ";
+ ActivityStarter setCaller(IApplicationThread caller) {
+ mRequest.caller = caller;
+ return this;
+ }
- if (dumpPackage != null) {
- if ((mLastStartActivityRecord[0] == null ||
- !dumpPackage.equals(mLastHomeActivityStartRecord[0].packageName)) &&
- (mLastHomeActivityStartRecord[0] == null ||
- !dumpPackage.equals(mLastHomeActivityStartRecord[0].packageName)) &&
- (mStartActivity == null || !dumpPackage.equals(mStartActivity.packageName))) {
- pw.print(prefix);
- pw.println("(nothing)");
- return;
- }
- }
+ ActivityStarter setEphemeralIntent(Intent intent) {
+ mRequest.ephemeralIntent = intent;
+ return this;
+ }
+
+
+ ActivityStarter setResolvedType(String type) {
+ mRequest.resolvedType = type;
+ return this;
+ }
+
+ ActivityStarter setActivityInfo(ActivityInfo info) {
+ mRequest.activityInfo = info;
+ return this;
+ }
+
+ ActivityStarter setResolveInfo(ResolveInfo info) {
+ mRequest.resolveInfo = info;
+ return this;
+ }
+
+ ActivityStarter setVoiceSession(IVoiceInteractionSession voiceSession) {
+ mRequest.voiceSession = voiceSession;
+ return this;
+ }
+
+ ActivityStarter setVoiceInteractor(IVoiceInteractor voiceInteractor) {
+ mRequest.voiceInteractor = voiceInteractor;
+ return this;
+ }
+
+ ActivityStarter setResultTo(IBinder resultTo) {
+ mRequest.resultTo = resultTo;
+ return this;
+ }
+
+ ActivityStarter setResultWho(String resultWho) {
+ mRequest.resultWho = resultWho;
+ return this;
+ }
+
+ ActivityStarter setRequestCode(int requestCode) {
+ mRequest.requestCode = requestCode;
+ return this;
+ }
+
+ ActivityStarter setCallingPid(int pid) {
+ mRequest.callingPid = pid;
+ return this;
+ }
+
+ ActivityStarter setCallingUid(int uid) {
+ mRequest.callingUid = uid;
+ return this;
+ }
+
+ ActivityStarter setCallingPackage(String callingPackage) {
+ mRequest.callingPackage = callingPackage;
+ return this;
+ }
+
+ ActivityStarter setRealCallingPid(int pid) {
+ mRequest.realCallingPid = pid;
+ return this;
+ }
+
+ ActivityStarter setRealCallingUid(int uid) {
+ mRequest.realCallingUid = uid;
+ return this;
+ }
+ ActivityStarter setStartFlags(int startFlags) {
+ mRequest.startFlags = startFlags;
+ return this;
+ }
+
+ ActivityStarter setActivityOptions(ActivityOptions options) {
+ mRequest.activityOptions = options;
+ return this;
+ }
+
+ ActivityStarter setIgnoreTargetSecurity(boolean ignoreTargetSecurity) {
+ mRequest.ignoreTargetSecurity = ignoreTargetSecurity;
+ return this;
+ }
+
+ ActivityStarter setComponentSpecified(boolean componentSpecified) {
+ mRequest.componentSpecified = componentSpecified;
+ return this;
+ }
+
+ ActivityStarter setOutActivity(ActivityRecord[] outActivity) {
+ mRequest.outActivity = outActivity;
+ return this;
+ }
+
+ ActivityStarter setInTask(TaskRecord inTask) {
+ mRequest.inTask = inTask;
+ return this;
+ }
+
+ ActivityStarter setWaitResult(WaitResult result) {
+ mRequest.waitResult = result;
+ return this;
+ }
+
+ ActivityStarter setProfilerInfo(ProfilerInfo info) {
+ mRequest.profilerInfo = info;
+ return this;
+ }
+
+ ActivityStarter setGlobalConfiguration(Configuration config) {
+ mRequest.globalConfig = config;
+ return this;
+ }
+
+ ActivityStarter setWaitOptions(Bundle options) {
+ mRequest.waitOptions = options;
+ return this;
+ }
+
+ ActivityStarter setUserId(int userId) {
+ mRequest.userId = userId;
+ return this;
+ }
+
+ ActivityStarter setMayWait(Bundle options, int userId) {
+ mRequest.mayWait = true;
+ mRequest.waitOptions = options;
+ mRequest.userId = userId;
+
+ return this;
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ prefix = prefix + " ";
pw.print(prefix);
pw.print("mCurrentUser=");
pw.println(mSupervisor.mCurrentUser);
@@ -2213,15 +2506,6 @@ class ActivityStarter {
pw.println("mLastStartActivityRecord:");
r.dump(pw, prefix + " ");
}
- pw.print(prefix);
- pw.print("mLastHomeActivityStartResult=");
- pw.println(mLastHomeActivityStartResult);
- r = mLastHomeActivityStartRecord[0];
- if (r != null) {
- pw.print(prefix);
- pw.println("mLastHomeActivityStartRecord:");
- r.dump(pw, prefix + " ");
- }
if (mStartActivity != null) {
pw.print(prefix);
pw.println("mStartActivity:");
diff --git a/com/android/server/am/AppErrors.java b/com/android/server/am/AppErrors.java
index fe380970..35465a79 100644
--- a/com/android/server/am/AppErrors.java
+++ b/com/android/server/am/AppErrors.java
@@ -407,10 +407,10 @@ class AppErrors {
// recents entry. Let's see if we have a safe-to-restart intent.
final Set<String> cats = task.intent.getCategories();
if (cats != null && cats.contains(Intent.CATEGORY_LAUNCHER)) {
- mService.startActivityInPackage(task.mCallingUid,
- task.mCallingPackage, task.intent, null, null, null, 0, 0,
- ActivityOptions.makeBasic().toBundle(), task.userId, null,
- "AppErrors");
+ mService.getActivityStartController().startActivityInPackage(
+ task.mCallingUid, task.mCallingPackage, task.intent, null, null,
+ null, 0, 0, ActivityOptions.makeBasic().toBundle(), task.userId,
+ null, "AppErrors");
}
}
}
@@ -521,13 +521,13 @@ class AppErrors {
*
* @param app The ProcessRecord in which the error occurred.
* @param condition Crashing, Application Not Responding, etc. Values are defined in
- * ActivityManager.AppErrorStateInfo
+ * ActivityManager.ProcessErrorStateInfo
* @param activity The activity associated with the crash, if known.
* @param shortMsg Short message describing the crash.
* @param longMsg Long message describing the crash.
* @param stackTrace Full crash stack trace, may be null.
*
- * @return Returns a fully-formed AppErrorStateInfo record.
+ * @return Returns a fully-formed ProcessErrorStateInfo record.
*/
private ActivityManager.ProcessErrorStateInfo generateProcessError(ProcessRecord app,
int condition, String activity, String shortMsg, String longMsg, String stackTrace) {
diff --git a/com/android/server/am/AppTaskImpl.java b/com/android/server/am/AppTaskImpl.java
index ab86dbdb..f821f6bd 100644
--- a/com/android/server/am/AppTaskImpl.java
+++ b/com/android/server/am/AppTaskImpl.java
@@ -122,9 +122,14 @@ class AppTaskImpl extends IAppTask.Stub {
throw new IllegalArgumentException("Bad app thread " + appThread);
}
}
- return mService.mActivityStarter.startActivityMayWait(appThread, -1, callingPackage,
- intent, resolvedType, null, null, null, null, 0, 0, null, null,
- null, bOptions, false, callingUser, tr, "AppTaskImpl");
+
+ return mService.getActivityStartController().obtainStarter(intent, "AppTaskImpl")
+ .setCaller(appThread)
+ .setCallingPackage(callingPackage)
+ .setResolvedType(resolvedType)
+ .setMayWait(bOptions, callingUser)
+ .setInTask(tr)
+ .execute();
}
@Override
diff --git a/com/android/server/am/BatteryExternalStatsWorker.java b/com/android/server/am/BatteryExternalStatsWorker.java
index f3ccba54..45824309 100644
--- a/com/android/server/am/BatteryExternalStatsWorker.java
+++ b/com/android/server/am/BatteryExternalStatsWorker.java
@@ -116,6 +116,50 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
return scheduleSyncLocked("remove-uid", UPDATE_CPU);
}
+ @Override
+ public Future<?> scheduleReadProcStateCpuTimes() {
+ synchronized (mStats) {
+ if (!mStats.mPerProcStateCpuTimesAvailable) {
+ return null;
+ }
+ }
+ synchronized (BatteryExternalStatsWorker.this) {
+ if (!mExecutorService.isShutdown()) {
+ return mExecutorService.submit(mReadProcStateCpuTimesTask);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Future<?> scheduleCopyFromAllUidsCpuTimes() {
+ synchronized (mStats) {
+ if (!mStats.mPerProcStateCpuTimesAvailable) {
+ return null;
+ }
+ }
+ synchronized (BatteryExternalStatsWorker.this) {
+ if (!mExecutorService.isShutdown()) {
+ return mExecutorService.submit(mCopyFromAllUidsCpuTimesTask);
+ }
+ }
+ return null;
+ }
+
+ private final Runnable mReadProcStateCpuTimesTask = new Runnable() {
+ @Override
+ public void run() {
+ mStats.updateProcStateCpuTimes();
+ }
+ };
+
+ private final Runnable mCopyFromAllUidsCpuTimesTask = new Runnable() {
+ @Override
+ public void run() {
+ mStats.copyFromAllUidsCpuTimes();
+ }
+ };
+
public synchronized Future<?> scheduleWrite() {
if (mExecutorService.isShutdown()) {
return CompletableFuture.failedFuture(new IllegalStateException("worker shutdown"));
@@ -185,6 +229,10 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
}
}
+ if ((updateFlags & UPDATE_CPU) != 0) {
+ mStats.copyFromAllUidsCpuTimes();
+ }
+
// Clean up any UIDs if necessary.
synchronized (mStats) {
for (int uid : uidsToRemove) {
diff --git a/com/android/server/am/BatteryStatsService.java b/com/android/server/am/BatteryStatsService.java
index a035bd02..35318f65 100644
--- a/com/android/server/am/BatteryStatsService.java
+++ b/com/android/server/am/BatteryStatsService.java
@@ -38,6 +38,7 @@ import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManagerInternal;
import android.os.WorkSource;
+import android.os.connectivity.CellularBatteryStats;
import android.os.health.HealthStatsParceler;
import android.os.health.HealthStatsWriter;
import android.os.health.UidHealthStats;
@@ -207,6 +208,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
}
+ private void syncStats(String reason, int flags) {
+ awaitUninterruptibly(mWorker.scheduleSync(reason, flags));
+ }
+
/**
* At the time when the constructor runs, the power manager has not yet been
* initialized. So we initialize the low power observer later.
@@ -225,7 +230,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
public void shutdown() {
Slog.w("BatteryStats", "Writing battery stats before shutdown...");
- awaitUninterruptibly(mWorker.scheduleSync("shutdown", BatteryExternalStatsWorker.UPDATE_ALL));
+ syncStats("shutdown", BatteryExternalStatsWorker.UPDATE_ALL);
synchronized (mStats) {
mStats.shutdownLocked();
@@ -357,7 +362,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
//Slog.i("foo", "SENDING BATTERY INFO:");
//mStats.dumpLocked(new LogPrinter(Log.INFO, "foo", Log.LOG_ID_SYSTEM));
Parcel out = Parcel.obtain();
- awaitUninterruptibly(mWorker.scheduleSync("get-stats", BatteryExternalStatsWorker.UPDATE_ALL));
+ syncStats("get-stats", BatteryExternalStatsWorker.UPDATE_ALL);
synchronized (mStats) {
mStats.writeToParcel(out, 0);
}
@@ -372,7 +377,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
//Slog.i("foo", "SENDING BATTERY INFO:");
//mStats.dumpLocked(new LogPrinter(Log.INFO, "foo", Log.LOG_ID_SYSTEM));
Parcel out = Parcel.obtain();
- awaitUninterruptibly(mWorker.scheduleSync("get-stats", BatteryExternalStatsWorker.UPDATE_ALL));
+ syncStats("get-stats", BatteryExternalStatsWorker.UPDATE_ALL);
synchronized (mStats) {
mStats.writeToParcel(out, 0);
}
@@ -459,15 +464,16 @@ public final class BatteryStatsService extends IBatteryStats.Stub
boolean unimportantForLogging) {
enforceCallingPermission();
synchronized (mStats) {
- mStats.noteStartWakeLocked(uid, pid, name, historyName, type, unimportantForLogging,
- SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());
+ mStats.noteStartWakeLocked(uid, pid, null, name, historyName, type,
+ unimportantForLogging, SystemClock.elapsedRealtime(),
+ SystemClock.uptimeMillis());
}
}
public void noteStopWakelock(int uid, int pid, String name, String historyName, int type) {
enforceCallingPermission();
synchronized (mStats) {
- mStats.noteStopWakeLocked(uid, pid, name, historyName, type,
+ mStats.noteStopWakeLocked(uid, pid, null, name, historyName, type,
SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());
}
}
@@ -932,11 +938,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub
enforceCallingPermission();
synchronized (mStats) {
mStats.noteDeviceIdleModeLocked(mode, activeReason, activeUid);
- StatsLog.write(StatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mode);
}
}
- public void notePackageInstalled(String pkgName, int versionCode) {
+ public void notePackageInstalled(String pkgName, long versionCode) {
enforceCallingPermission();
synchronized (mStats) {
mStats.notePackageInstalledLocked(pkgName, versionCode);
@@ -1238,8 +1243,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
mWorker.scheduleSync("dump", BatteryExternalStatsWorker.UPDATE_ALL);
} else if ("--write".equals(arg)) {
- awaitUninterruptibly(mWorker.scheduleSync("dump",
- BatteryExternalStatsWorker.UPDATE_ALL));
+ syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL);
synchronized (mStats) {
mStats.writeSyncLocked();
pw.println("Battery stats written.");
@@ -1303,7 +1307,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
flags |= BatteryStats.DUMP_DEVICE_WIFI_ONLY;
}
// Fetch data from external sources and update the BatteryStatsImpl object with them.
- awaitUninterruptibly(mWorker.scheduleSync("dump", BatteryExternalStatsWorker.UPDATE_ALL));
+ syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1336,8 +1340,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
null, mStats.mHandler, null, mUserManagerUserInfoProvider);
checkinStats.readSummaryFromParcel(in);
in.recycle();
- checkinStats.dumpProtoLocked(mContext, fd, apps, flags,
- historyStart);
+ checkinStats.dumpProtoLocked(mContext, fd, apps, flags);
mStats.mCheckinFile.delete();
return;
}
@@ -1350,7 +1353,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
if (DBG) Slog.d(TAG, "begin dumpProtoLocked from UID " + Binder.getCallingUid());
synchronized (mStats) {
- mStats.dumpProtoLocked(mContext, fd, apps, flags, historyStart);
+ mStats.dumpProtoLocked(mContext, fd, apps, flags);
if (writeData) {
mStats.writeAsyncLocked();
}
@@ -1407,6 +1410,16 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
/**
+ * Gets a snapshot of cellular stats
+ * @hide
+ */
+ public CellularBatteryStats getCellularBatteryStats() {
+ synchronized (mStats) {
+ return mStats.getCellularBatteryStats();
+ }
+ }
+
+ /**
* Gets a snapshot of the system health for a particular uid.
*/
@Override
@@ -1417,8 +1430,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
long ident = Binder.clearCallingIdentity();
try {
- awaitUninterruptibly(mWorker.scheduleSync("get-health-stats-for-uids",
- BatteryExternalStatsWorker.UPDATE_ALL));
+ syncStats("get-health-stats-for-uids", BatteryExternalStatsWorker.UPDATE_ALL);
synchronized (mStats) {
return getHealthStatsForUidLocked(requestUid);
}
@@ -1442,8 +1454,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
long ident = Binder.clearCallingIdentity();
int i=-1;
try {
- awaitUninterruptibly(mWorker.scheduleSync("get-health-stats-for-uids",
- BatteryExternalStatsWorker.UPDATE_ALL));
+ syncStats("get-health-stats-for-uids", BatteryExternalStatsWorker.UPDATE_ALL);
synchronized (mStats) {
final int N = requestUids.length;
final HealthStatsParceler[] results = new HealthStatsParceler[N];
diff --git a/com/android/server/am/BroadcastQueue.java b/com/android/server/am/BroadcastQueue.java
index aa82d000..ea90db3f 100644
--- a/com/android/server/am/BroadcastQueue.java
+++ b/com/android/server/am/BroadcastQueue.java
@@ -322,7 +322,7 @@ public final class BroadcastQueue {
public boolean sendPendingBroadcastsLocked(ProcessRecord app) {
boolean didSomething = false;
final BroadcastRecord br = mPendingBroadcast;
- if (br != null && br.curApp.pid == app.pid) {
+ if (br != null && br.curApp.pid > 0 && br.curApp.pid == app.pid) {
if (br.curApp != app) {
Slog.e(TAG, "App mismatch when sending pending broadcast to "
+ app.processName + ", intended target is " + br.curApp.processName);
@@ -876,9 +876,16 @@ public final class BroadcastQueue {
+ mPendingBroadcast.curApp);
boolean isDead;
- synchronized (mService.mPidsSelfLocked) {
- ProcessRecord proc = mService.mPidsSelfLocked.get(mPendingBroadcast.curApp.pid);
- isDead = proc == null || proc.crashing;
+ if (mPendingBroadcast.curApp.pid > 0) {
+ synchronized (mService.mPidsSelfLocked) {
+ ProcessRecord proc = mService.mPidsSelfLocked.get(
+ mPendingBroadcast.curApp.pid);
+ isDead = proc == null || proc.crashing;
+ }
+ } else {
+ final ProcessRecord proc = mService.mProcessNames.get(
+ mPendingBroadcast.curApp.processName, mPendingBroadcast.curApp.uid);
+ isDead = proc == null || !proc.pendingStart;
}
if (!isDead) {
// It's still alive, so keep waiting
diff --git a/com/android/server/am/ClientLifecycleManager.java b/com/android/server/am/ClientLifecycleManager.java
index c04d103c..1e708098 100644
--- a/com/android/server/am/ClientLifecycleManager.java
+++ b/com/android/server/am/ClientLifecycleManager.java
@@ -21,6 +21,7 @@ import android.app.IApplicationThread;
import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.ClientTransactionItem;
import android.app.servertransaction.ActivityLifecycleItem;
+import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
@@ -43,6 +44,12 @@ class ClientLifecycleManager {
*/
void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
transaction.schedule();
+ if (!(transaction.getClient() instanceof Binder)) {
+ // If client is not an instance of Binder - it's a remote call and at this point it is
+ // safe to recycle the object. All objects used for local calls will be recycled after
+ // the transaction is executed on client in ActivityThread.
+ transaction.recycle();
+ }
}
/**
@@ -100,7 +107,7 @@ class ClientLifecycleManager {
*/
private static ClientTransaction transactionWithState(@NonNull IApplicationThread client,
@NonNull IBinder activityToken, @NonNull ActivityLifecycleItem stateRequest) {
- final ClientTransaction clientTransaction = new ClientTransaction(client, activityToken);
+ final ClientTransaction clientTransaction = ClientTransaction.obtain(client, activityToken);
clientTransaction.setLifecycleStateRequest(stateRequest);
return clientTransaction;
}
@@ -113,7 +120,7 @@ class ClientLifecycleManager {
*/
private static ClientTransaction transactionWithCallback(@NonNull IApplicationThread client,
IBinder activityToken, @NonNull ClientTransactionItem callback) {
- final ClientTransaction clientTransaction = new ClientTransaction(client, activityToken);
+ final ClientTransaction clientTransaction = ClientTransaction.obtain(client, activityToken);
clientTransaction.addCallback(callback);
return clientTransaction;
}
diff --git a/com/android/server/am/LockTaskController.java b/com/android/server/am/LockTaskController.java
index d77e1a20..ba3e25ae 100644
--- a/com/android/server/am/LockTaskController.java
+++ b/com/android/server/am/LockTaskController.java
@@ -22,8 +22,10 @@ import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.Context.DEVICE_POLICY_SERVICE;
import static android.content.Context.STATUS_BAR_SERVICE;
+import static android.content.Intent.ACTION_CALL_EMERGENCY;
import static android.os.UserHandle.USER_ALL;
import static android.os.UserHandle.USER_CURRENT;
+import static android.telecom.TelecomManager.EMERGENCY_DIALER_COMPONENT;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_LOCKTASK;
@@ -45,6 +47,7 @@ import android.app.admin.DevicePolicyManager;
import android.app.admin.IDevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.os.Binder;
import android.os.Debug;
import android.os.Handler;
@@ -52,9 +55,11 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.provider.Settings;
+import android.telecom.TelecomManager;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseIntArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
@@ -133,6 +138,8 @@ public class LockTaskController {
WindowManagerService mWindowManager;
@VisibleForTesting
LockPatternUtils mLockPatternUtils;
+ @VisibleForTesting
+ TelecomManager mTelecomManager;
/**
* Helper that is responsible for showing the right toast when a disallowed activity operation
@@ -165,7 +172,7 @@ public class LockTaskController {
/**
* Features that are allowed by DPC to show during LockTask mode.
*/
- private final SparseArray<Integer> mLockTaskFeatures = new SparseArray<>();
+ private final SparseIntArray mLockTaskFeatures = new SparseIntArray();
/**
* Store the current lock task mode. Possible values:
@@ -298,6 +305,11 @@ public class LockTaskController {
return false;
}
+ // Allow emergency calling when the device is protected by a locked keyguard
+ if (isKeyguardAllowed(task.userId) && isEmergencyCallTask(task)) {
+ return false;
+ }
+
return !(isTaskWhitelisted(task) || mLockTaskModeTasks.isEmpty());
}
@@ -306,6 +318,37 @@ public class LockTaskController {
& DevicePolicyManager.LOCK_TASK_FEATURE_RECENTS) != 0;
}
+ private boolean isKeyguardAllowed(int userId) {
+ return (getLockTaskFeaturesForUser(userId)
+ & DevicePolicyManager.LOCK_TASK_FEATURE_KEYGUARD) != 0;
+ }
+
+ private boolean isEmergencyCallTask(TaskRecord task) {
+ final Intent intent = task.intent;
+ if (intent == null) {
+ return false;
+ }
+
+ // 1. The emergency keypad activity launched on top of the keyguard
+ if (EMERGENCY_DIALER_COMPONENT.equals(intent.getComponent())) {
+ return true;
+ }
+
+ // 2. The intent sent by the keypad, which is handled by Telephony
+ if (ACTION_CALL_EMERGENCY.equals(intent.getAction())) {
+ return true;
+ }
+
+ // 3. Telephony then starts the default package for making the call
+ final TelecomManager tm = getTelecomManager();
+ final String dialerPackage = tm != null ? tm.getSystemDialerPackage() : null;
+ if (dialerPackage != null && dialerPackage.equals(intent.getComponent().getPackageName())) {
+ return true;
+ }
+
+ return false;
+ }
+
/**
* Stop the current lock task mode.
*
@@ -686,11 +729,10 @@ public class LockTaskController {
mWindowManager.reenableKeyguard(mToken);
} else if (lockTaskModeState == LOCK_TASK_MODE_LOCKED) {
- int lockTaskFeatures = getLockTaskFeaturesForUser(userId);
- if ((DevicePolicyManager.LOCK_TASK_FEATURE_KEYGUARD & lockTaskFeatures) == 0) {
- mWindowManager.disableKeyguard(mToken, LOCK_TASK_TAG);
- } else {
+ if (isKeyguardAllowed(userId)) {
mWindowManager.reenableKeyguard(mToken);
+ } else {
+ mWindowManager.disableKeyguard(mToken, LOCK_TASK_TAG);
}
} else { // lockTaskModeState == LOCK_TASK_MODE_PINNED
@@ -784,6 +826,15 @@ public class LockTaskController {
return mLockPatternUtils;
}
+ @Nullable
+ private TelecomManager getTelecomManager() {
+ if (mTelecomManager == null) {
+ // We don't preserve the TelecomManager object to save memory
+ return mContext.getSystemService(TelecomManager.class);
+ }
+ return mTelecomManager;
+ }
+
// Should only be called on the handler thread
@NonNull
private LockTaskNotify getLockTaskNotify() {
diff --git a/com/android/server/am/PendingIntentRecord.java b/com/android/server/am/PendingIntentRecord.java
index 7930f534..c26e7703 100644
--- a/com/android/server/am/PendingIntentRecord.java
+++ b/com/android/server/am/PendingIntentRecord.java
@@ -332,12 +332,14 @@ final class PendingIntentRecord extends IIntentSender.Stub {
}
allIntents[allIntents.length-1] = finalIntent;
allResolvedTypes[allResolvedTypes.length-1] = resolvedType;
- owner.startActivitiesInPackage(uid, key.packageName, allIntents,
- allResolvedTypes, resultTo, options, userId);
+ owner.getActivityStartController().startActivitiesInPackage(uid,
+ key.packageName, allIntents, allResolvedTypes, resultTo,
+ options, userId);
} else {
- owner.startActivityInPackage(uid, key.packageName, finalIntent,
- resolvedType, resultTo, resultWho, requestCode, 0,
- options, userId, null, "PendingIntentRecord");
+ owner.getActivityStartController().startActivityInPackage(uid,
+ key.packageName, finalIntent, resolvedType, resultTo,
+ resultWho, requestCode, 0, options, userId, null,
+ "PendingIntentRecord");
}
} catch (RuntimeException e) {
Slog.w(TAG, "Unable to send startActivity intent", e);
diff --git a/com/android/server/am/ProcessList.java b/com/android/server/am/ProcessList.java
index 6fb3dbb7..ab5d64c4 100644
--- a/com/android/server/am/ProcessList.java
+++ b/com/android/server/am/ProcessList.java
@@ -364,9 +364,6 @@ public final class ProcessList {
case ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE:
procState = "FGS ";
break;
- case ActivityManager.PROCESS_STATE_TOP_SLEEPING:
- procState = "TPSL";
- break;
case ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND:
procState = "IMPF";
break;
@@ -379,15 +376,18 @@ public final class ProcessList {
case ActivityManager.PROCESS_STATE_BACKUP:
procState = "BKUP";
break;
- case ActivityManager.PROCESS_STATE_HEAVY_WEIGHT:
- procState = "HVY ";
- break;
case ActivityManager.PROCESS_STATE_SERVICE:
procState = "SVC ";
break;
case ActivityManager.PROCESS_STATE_RECEIVER:
procState = "RCVR";
break;
+ case ActivityManager.PROCESS_STATE_TOP_SLEEPING:
+ procState = "TPSL";
+ break;
+ case ActivityManager.PROCESS_STATE_HEAVY_WEIGHT:
+ procState = "HVY ";
+ break;
case ActivityManager.PROCESS_STATE_HOME:
procState = "HOME";
break;
@@ -485,14 +485,14 @@ public final class ProcessList {
PROC_MEM_TOP, // ActivityManager.PROCESS_STATE_TOP
PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
- PROC_MEM_TOP, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_BACKUP
- PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PROC_MEM_SERVICE, // ActivityManager.PROCESS_STATE_SERVICE
PROC_MEM_CACHED, // ActivityManager.PROCESS_STATE_RECEIVER
+ PROC_MEM_TOP, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+ PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PROC_MEM_CACHED, // ActivityManager.PROCESS_STATE_HOME
PROC_MEM_CACHED, // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
PROC_MEM_CACHED, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
@@ -507,14 +507,14 @@ public final class ProcessList {
PSS_FIRST_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_TOP
PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
- PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_BACKUP
- PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_SERVICE
PSS_FIRST_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_RECEIVER
+ PSS_FIRST_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+ PSS_FIRST_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PSS_FIRST_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_HOME
PSS_FIRST_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
PSS_FIRST_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
@@ -529,14 +529,14 @@ public final class ProcessList {
PSS_SHORT_INTERVAL, // ActivityManager.PROCESS_STATE_TOP
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
- PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_BACKUP
- PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PSS_SAME_SERVICE_INTERVAL, // ActivityManager.PROCESS_STATE_SERVICE
PSS_SAME_SERVICE_INTERVAL, // ActivityManager.PROCESS_STATE_RECEIVER
+ PSS_SAME_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+ PSS_SAME_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PSS_SAME_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_HOME
PSS_SAME_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
PSS_SAME_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
@@ -545,20 +545,20 @@ public final class ProcessList {
PSS_SAME_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_EMPTY
};
- private static final long[] sTestFirstAwakePssTimes = new long[] {
+ private static final long[] sTestFirstPssTimes = new long[] {
PSS_TEST_FIRST_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT
PSS_TEST_FIRST_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
PSS_TEST_FIRST_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_TOP
- PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
- PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
- PSS_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+ PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
+ PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_BACKUP
- PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_SERVICE
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_RECEIVER
+ PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+ PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_HOME
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
@@ -567,20 +567,20 @@ public final class ProcessList {
PSS_TEST_FIRST_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_EMPTY
};
- private static final long[] sTestSameAwakePssTimes = new long[] {
+ private static final long[] sTestSamePssTimes = new long[] {
PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT
PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_TOP
PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
- PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_BACKUP
- PSS_TEST_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_SERVICE
PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_RECEIVER
+ PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+ PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_HOME
PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
PSS_TEST_SAME_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
@@ -601,8 +601,8 @@ public final class ProcessList {
boolean sleeping, long now) {
final long[] table = test
? (first
- ? sTestFirstAwakePssTimes
- : sTestSameAwakePssTimes)
+ ? sTestFirstPssTimes
+ : sTestSamePssTimes)
: (first
? sFirstAwakePssTimes
: sSameAwakePssTimes);
@@ -628,6 +628,7 @@ public final class ProcessList {
/**
* Set the out-of-memory badness adjustment for a process.
+ * If {@code pid <= 0}, this method will be a no-op.
*
* @param pid The process identifier to set.
* @param uid The uid of the app
@@ -636,6 +637,10 @@ public final class ProcessList {
* {@hide}
*/
public static final void setOomAdj(int pid, int uid, int amt) {
+ // This indicates that the process is not started yet and so no need to proceed further.
+ if (pid <= 0) {
+ return;
+ }
if (amt == UNKNOWN_ADJ)
return;
@@ -657,6 +662,10 @@ public final class ProcessList {
* {@hide}
*/
public static final void remove(int pid) {
+ // This indicates that the process is not started yet and so no need to proceed further.
+ if (pid <= 0) {
+ return;
+ }
ByteBuffer buf = ByteBuffer.allocate(4 * 2);
buf.putInt(LMK_PROCREMOVE);
buf.putInt(pid);
diff --git a/com/android/server/am/ProcessRecord.java b/com/android/server/am/ProcessRecord.java
index 9d3c2ae3..a1e59472 100644
--- a/com/android/server/am/ProcessRecord.java
+++ b/com/android/server/am/ProcessRecord.java
@@ -196,6 +196,9 @@ final class ProcessRecord {
String shortStringName; // caching of toShortString() result.
String stringName; // caching of toString() result.
+ boolean pendingStart; // Process start is pending.
+ long startSeq; // Seq no. indicating the latest process start associated with
+ // this process record.
// These reports are generated & stored when an app gets into an error condition.
// They will be "null" when all is OK.
@@ -211,6 +214,23 @@ final class ProcessRecord {
// App is allowed to manage whitelists such as temporary Power Save mode whitelist.
boolean whitelistManager;
+ // Params used in starting this process.
+ String hostingType;
+ String hostingNameStr;
+ String seInfo;
+ long startTime;
+ // This will be same as {@link #uid} usually except for some apps used during factory testing.
+ int startUid;
+
+ void setStartParams(int startUid, String hostingType, String hostingNameStr, String seInfo,
+ long startTime) {
+ this.startUid = startUid;
+ this.hostingType = hostingType;
+ this.hostingNameStr = hostingNameStr;
+ this.seInfo = seInfo;
+ this.startTime = startTime;
+ }
+
void dump(PrintWriter pw, String prefix) {
final long nowUptime = SystemClock.uptimeMillis();
@@ -348,6 +368,10 @@ final class ProcessRecord {
if (hasStartedServices) {
pw.print(prefix); pw.print("hasStartedServices="); pw.println(hasStartedServices);
}
+ if (pendingStart) {
+ pw.print(prefix); pw.print("pendingStart="); pw.println(pendingStart);
+ }
+ pw.print(prefix); pw.print("startSeq="); pw.println(startSeq);
if (setProcState > ActivityManager.PROCESS_STATE_SERVICE) {
pw.print(prefix); pw.print("lastCpuTime="); pw.print(lastCpuTime);
if (lastCpuTime > 0) {
@@ -627,9 +651,13 @@ final class ProcessRecord {
if (noisy) {
Slog.i(TAG, "Killing " + toShortString() + " (adj " + setAdj + "): " + reason);
}
- EventLog.writeEvent(EventLogTags.AM_KILL, userId, pid, processName, setAdj, reason);
- Process.killProcessQuiet(pid);
- ActivityManagerService.killProcessGroup(uid, pid);
+ if (pid > 0) {
+ EventLog.writeEvent(EventLogTags.AM_KILL, userId, pid, processName, setAdj, reason);
+ Process.killProcessQuiet(pid);
+ ActivityManagerService.killProcessGroup(uid, pid);
+ } else {
+ pendingStart = false;
+ }
if (!persistent) {
killed = true;
killedByAm = true;
@@ -730,7 +758,7 @@ final class ProcessRecord {
/*
* Return true if package has been added false if not
*/
- public boolean addPackage(String pkg, int versionCode, ProcessStatsService tracker) {
+ public boolean addPackage(String pkg, long versionCode, ProcessStatsService tracker) {
if (!pkgList.containsKey(pkg)) {
ProcessStats.ProcessStateHolder holder = new ProcessStats.ProcessStateHolder(
versionCode);
diff --git a/com/android/server/am/ProcessStatsService.java b/com/android/server/am/ProcessStatsService.java
index effb86c1..5f9d6162 100644
--- a/com/android/server/am/ProcessStatsService.java
+++ b/com/android/server/am/ProcessStatsService.java
@@ -27,6 +27,7 @@ import android.service.procstats.ProcessStatsProto;
import android.service.procstats.ProcessStatsServiceDumpProto;
import android.util.ArrayMap;
import android.util.AtomicFile;
+import android.util.LongSparseArray;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
@@ -120,12 +121,12 @@ public final class ProcessStatsService extends IProcessStats.Stub {
}
public ProcessState getProcessStateLocked(String packageName,
- int uid, int versionCode, String processName) {
+ int uid, long versionCode, String processName) {
return mProcessStats.getProcessStateLocked(packageName, uid, versionCode, processName);
}
public ServiceState getServiceStateLocked(String packageName, int uid,
- int versionCode, String processName, String className) {
+ long versionCode, String processName, String className) {
return mProcessStats.getServiceStateLocked(packageName, uid, versionCode, processName,
className);
}
@@ -150,12 +151,13 @@ public final class ProcessStatsService extends IProcessStats.Stub {
}
mProcessStats.mMemFactor = memFactor;
mProcessStats.mStartTime = now;
- final ArrayMap<String, SparseArray<SparseArray<ProcessStats.PackageState>>> pmap
+ final ArrayMap<String, SparseArray<LongSparseArray<ProcessStats.PackageState>>> pmap
= mProcessStats.mPackages.getMap();
for (int ipkg=pmap.size()-1; ipkg>=0; ipkg--) {
- final SparseArray<SparseArray<ProcessStats.PackageState>> uids = pmap.valueAt(ipkg);
+ final SparseArray<LongSparseArray<ProcessStats.PackageState>> uids =
+ pmap.valueAt(ipkg);
for (int iuid=uids.size()-1; iuid>=0; iuid--) {
- final SparseArray<ProcessStats.PackageState> vers = uids.valueAt(iuid);
+ final LongSparseArray<ProcessStats.PackageState> vers = uids.valueAt(iuid);
for (int iver=vers.size()-1; iver>=0; iver--) {
final ProcessStats.PackageState pkg = vers.valueAt(iver);
final ArrayMap<String, ServiceState> services = pkg.mServices;
@@ -308,17 +310,17 @@ public final class ProcessStatsService extends IProcessStats.Stub {
Slog.w(TAG, " Uid " + uids.keyAt(iu) + ": " + uids.valueAt(iu));
}
}
- ArrayMap<String, SparseArray<SparseArray<ProcessStats.PackageState>>> pkgMap
+ ArrayMap<String, SparseArray<LongSparseArray<ProcessStats.PackageState>>> pkgMap
= stats.mPackages.getMap();
final int NPKG = pkgMap.size();
for (int ip=0; ip<NPKG; ip++) {
Slog.w(TAG, "Package: " + pkgMap.keyAt(ip));
- SparseArray<SparseArray<ProcessStats.PackageState>> uids
+ SparseArray<LongSparseArray<ProcessStats.PackageState>> uids
= pkgMap.valueAt(ip);
final int NUID = uids.size();
for (int iu=0; iu<NUID; iu++) {
Slog.w(TAG, " Uid: " + uids.keyAt(iu));
- SparseArray<ProcessStats.PackageState> vers = uids.valueAt(iu);
+ LongSparseArray<ProcessStats.PackageState> vers = uids.valueAt(iu);
final int NVERS = vers.size();
for (int iv=0; iv<NVERS; iv++) {
Slog.w(TAG, " Vers: " + vers.keyAt(iv));
diff --git a/com/android/server/am/RecentTasks.java b/com/android/server/am/RecentTasks.java
index abb296e9..2de84ab2 100644
--- a/com/android/server/am/RecentTasks.java
+++ b/com/android/server/am/RecentTasks.java
@@ -662,7 +662,7 @@ class RecentTasks {
* task to be trimmed as a result of that add.
*/
private boolean canAddTaskWithoutTrim(TaskRecord task) {
- return findTrimIndexForAddTask(task) == -1;
+ return findRemoveIndexForAddTask(task) == -1;
}
/**
@@ -896,7 +896,7 @@ class RecentTasks {
}
if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: trimming tasks for " + task);
- trimForAddTask(task);
+ removeForAddTask(task);
task.inRecents = true;
if (!isAffiliated || needAffiliationFix) {
@@ -1175,8 +1175,8 @@ class RecentTasks {
* If needed, remove oldest existing entries in recents that are for the same kind
* of task as the given one.
*/
- private void trimForAddTask(TaskRecord task) {
- final int removeIndex = findTrimIndexForAddTask(task);
+ private void removeForAddTask(TaskRecord task) {
+ final int removeIndex = findRemoveIndexForAddTask(task);
if (removeIndex == -1) {
// Nothing to trim
return;
@@ -1187,7 +1187,7 @@ class RecentTasks {
// callbacks here.
final TaskRecord removedTask = mTasks.remove(removeIndex);
if (removedTask != task) {
- notifyTaskRemoved(removedTask, TRIMMED);
+ notifyTaskRemoved(removedTask, !TRIMMED);
if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "Trimming task=" + removedTask
+ " for addition of task=" + task);
}
@@ -1198,7 +1198,7 @@ class RecentTasks {
* Find the task that would be removed if the given {@param task} is added to the recent tasks
* list (if any).
*/
- private int findTrimIndexForAddTask(TaskRecord task) {
+ private int findRemoveIndexForAddTask(TaskRecord task) {
int recentsCount = mTasks.size();
final Intent intent = task.intent;
final boolean document = intent != null && intent.isDocument();
@@ -1241,7 +1241,6 @@ class RecentTasks {
// don't need to trim it.
continue;
} else if (maxRecents > 0) {
- // Otherwise only trim if we are over our max recents for this task
--maxRecents;
if (!sameIntent || multiTasksAllowed) {
// We don't want to trim if we are not over the max allowed entries and
diff --git a/com/android/server/am/TaskRecord.java b/com/android/server/am/TaskRecord.java
index 83965ee4..4aef95d2 100644
--- a/com/android/server/am/TaskRecord.java
+++ b/com/android/server/am/TaskRecord.java
@@ -193,6 +193,11 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
// Do not move the stack as a part of reparenting
static final int REPARENT_LEAVE_STACK_IN_PLACE = 2;
+ /**
+ * The factory used to create {@link TaskRecord}. This allows OEM subclass {@link TaskRecord}.
+ */
+ private static TaskRecordFactory sTaskRecordFactory;
+
final int taskId; // Unique identifier for this task.
String affinity; // The affinity name for this task, or null; may change identity.
String rootAffinity; // Initial base affinity, or null; does not change from initial root.
@@ -312,6 +317,10 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
private TaskWindowContainerController mWindowContainerController;
+ /**
+ * Don't use constructor directly. Use {@link #create(ActivityManagerService, int, ActivityInfo,
+ * Intent, TaskDescription)} instead.
+ */
TaskRecord(ActivityManagerService service, int _taskId, ActivityInfo info, Intent _intent,
IVoiceInteractionSession _voiceSession, IVoiceInteractor _voiceInteractor) {
mService = service;
@@ -331,6 +340,10 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
mService.mTaskChangeNotificationController.notifyTaskCreated(_taskId, realActivity);
}
+ /**
+ * Don't use constructor directly. Use {@link #create(ActivityManagerService, int, ActivityInfo,
+ * Intent, IVoiceInteractionSession, IVoiceInteractor)} instead.
+ */
TaskRecord(ActivityManagerService service, int _taskId, ActivityInfo info, Intent _intent,
TaskDescription _taskDescription) {
mService = service;
@@ -357,7 +370,10 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
mService.mTaskChangeNotificationController.notifyTaskCreated(_taskId, realActivity);
}
- private TaskRecord(ActivityManagerService service, int _taskId, Intent _intent,
+ /**
+ * Don't use constructor directly. This is only used by XML parser.
+ */
+ TaskRecord(ActivityManagerService service, int _taskId, Intent _intent,
Intent _affinityIntent, String _affinity, String _rootAffinity,
ComponentName _realActivity, ComponentName _origActivity, boolean _rootWasReset,
boolean _autoRemoveRecents, boolean _askedCompatMode, int _userId,
@@ -607,6 +623,9 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
if (toStack == sourceStack) {
return false;
}
+ if (!canBeLaunchedOnDisplay(toStack.mDisplayId)) {
+ return false;
+ }
final int toStackWindowingMode = toStack.getWindowingMode();
final ActivityRecord topActivity = getTopActivity();
@@ -753,10 +772,6 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
mWindowContainerController.cancelWindowTransition();
}
- void cancelThumbnailTransition() {
- mWindowContainerController.cancelThumbnailTransition();
- }
-
/**
* DO NOT HOLD THE ACTIVITY MANAGER LOCK WHEN CALLING THIS METHOD!
*/
@@ -1632,278 +1647,6 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
updateTaskDescription();
}
- void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
- if (DEBUG_RECENTS) Slog.i(TAG_RECENTS, "Saving task=" + this);
-
- out.attribute(null, ATTR_TASKID, String.valueOf(taskId));
- if (realActivity != null) {
- out.attribute(null, ATTR_REALACTIVITY, realActivity.flattenToShortString());
- }
- out.attribute(null, ATTR_REALACTIVITY_SUSPENDED, String.valueOf(realActivitySuspended));
- if (origActivity != null) {
- out.attribute(null, ATTR_ORIGACTIVITY, origActivity.flattenToShortString());
- }
- // Write affinity, and root affinity if it is different from affinity.
- // We use the special string "@" for a null root affinity, so we can identify
- // later whether we were given a root affinity or should just make it the
- // same as the affinity.
- if (affinity != null) {
- out.attribute(null, ATTR_AFFINITY, affinity);
- if (!affinity.equals(rootAffinity)) {
- out.attribute(null, ATTR_ROOT_AFFINITY, rootAffinity != null ? rootAffinity : "@");
- }
- } else if (rootAffinity != null) {
- out.attribute(null, ATTR_ROOT_AFFINITY, rootAffinity != null ? rootAffinity : "@");
- }
- out.attribute(null, ATTR_ROOTHASRESET, String.valueOf(rootWasReset));
- out.attribute(null, ATTR_AUTOREMOVERECENTS, String.valueOf(autoRemoveRecents));
- out.attribute(null, ATTR_ASKEDCOMPATMODE, String.valueOf(askedCompatMode));
- out.attribute(null, ATTR_USERID, String.valueOf(userId));
- out.attribute(null, ATTR_USER_SETUP_COMPLETE, String.valueOf(mUserSetupComplete));
- out.attribute(null, ATTR_EFFECTIVE_UID, String.valueOf(effectiveUid));
- out.attribute(null, ATTR_LASTTIMEMOVED, String.valueOf(mLastTimeMoved));
- out.attribute(null, ATTR_NEVERRELINQUISH, String.valueOf(mNeverRelinquishIdentity));
- if (lastDescription != null) {
- out.attribute(null, ATTR_LASTDESCRIPTION, lastDescription.toString());
- }
- if (lastTaskDescription != null) {
- lastTaskDescription.saveToXml(out);
- }
- out.attribute(null, ATTR_TASK_AFFILIATION_COLOR, String.valueOf(mAffiliatedTaskColor));
- out.attribute(null, ATTR_TASK_AFFILIATION, String.valueOf(mAffiliatedTaskId));
- out.attribute(null, ATTR_PREV_AFFILIATION, String.valueOf(mPrevAffiliateTaskId));
- out.attribute(null, ATTR_NEXT_AFFILIATION, String.valueOf(mNextAffiliateTaskId));
- out.attribute(null, ATTR_CALLING_UID, String.valueOf(mCallingUid));
- out.attribute(null, ATTR_CALLING_PACKAGE, mCallingPackage == null ? "" : mCallingPackage);
- out.attribute(null, ATTR_RESIZE_MODE, String.valueOf(mResizeMode));
- out.attribute(null, ATTR_SUPPORTS_PICTURE_IN_PICTURE,
- String.valueOf(mSupportsPictureInPicture));
- if (mLastNonFullscreenBounds != null) {
- out.attribute(
- null, ATTR_NON_FULLSCREEN_BOUNDS, mLastNonFullscreenBounds.flattenToString());
- }
- out.attribute(null, ATTR_MIN_WIDTH, String.valueOf(mMinWidth));
- out.attribute(null, ATTR_MIN_HEIGHT, String.valueOf(mMinHeight));
- out.attribute(null, ATTR_PERSIST_TASK_VERSION, String.valueOf(PERSIST_TASK_VERSION));
-
- if (affinityIntent != null) {
- out.startTag(null, TAG_AFFINITYINTENT);
- affinityIntent.saveToXml(out);
- out.endTag(null, TAG_AFFINITYINTENT);
- }
-
- out.startTag(null, TAG_INTENT);
- intent.saveToXml(out);
- out.endTag(null, TAG_INTENT);
-
- final ArrayList<ActivityRecord> activities = mActivities;
- final int numActivities = activities.size();
- for (int activityNdx = 0; activityNdx < numActivities; ++activityNdx) {
- final ActivityRecord r = activities.get(activityNdx);
- if (r.info.persistableMode == ActivityInfo.PERSIST_ROOT_ONLY || !r.isPersistable() ||
- ((r.intent.getFlags() & FLAG_ACTIVITY_NEW_DOCUMENT
- | FLAG_ACTIVITY_RETAIN_IN_RECENTS) == FLAG_ACTIVITY_NEW_DOCUMENT) &&
- activityNdx > 0) {
- // Stop at first non-persistable or first break in task (CLEAR_WHEN_TASK_RESET).
- break;
- }
- out.startTag(null, TAG_ACTIVITY);
- r.saveToXml(out);
- out.endTag(null, TAG_ACTIVITY);
- }
- }
-
- static TaskRecord restoreFromXml(XmlPullParser in, ActivityStackSupervisor stackSupervisor)
- throws IOException, XmlPullParserException {
- Intent intent = null;
- Intent affinityIntent = null;
- ArrayList<ActivityRecord> activities = new ArrayList<>();
- ComponentName realActivity = null;
- boolean realActivitySuspended = false;
- ComponentName origActivity = null;
- String affinity = null;
- String rootAffinity = null;
- boolean hasRootAffinity = false;
- boolean rootHasReset = false;
- boolean autoRemoveRecents = false;
- boolean askedCompatMode = false;
- int taskType = 0;
- int userId = 0;
- boolean userSetupComplete = true;
- int effectiveUid = -1;
- String lastDescription = null;
- long lastTimeOnTop = 0;
- boolean neverRelinquishIdentity = true;
- int taskId = INVALID_TASK_ID;
- final int outerDepth = in.getDepth();
- TaskDescription taskDescription = new TaskDescription();
- int taskAffiliation = INVALID_TASK_ID;
- int taskAffiliationColor = 0;
- int prevTaskId = INVALID_TASK_ID;
- int nextTaskId = INVALID_TASK_ID;
- int callingUid = -1;
- String callingPackage = "";
- int resizeMode = RESIZE_MODE_FORCE_RESIZEABLE;
- boolean supportsPictureInPicture = false;
- Rect bounds = null;
- int minWidth = INVALID_MIN_SIZE;
- int minHeight = INVALID_MIN_SIZE;
- int persistTaskVersion = 0;
-
- for (int attrNdx = in.getAttributeCount() - 1; attrNdx >= 0; --attrNdx) {
- final String attrName = in.getAttributeName(attrNdx);
- final String attrValue = in.getAttributeValue(attrNdx);
- if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, "TaskRecord: attribute name=" +
- attrName + " value=" + attrValue);
- if (ATTR_TASKID.equals(attrName)) {
- if (taskId == INVALID_TASK_ID) taskId = Integer.parseInt(attrValue);
- } else if (ATTR_REALACTIVITY.equals(attrName)) {
- realActivity = ComponentName.unflattenFromString(attrValue);
- } else if (ATTR_REALACTIVITY_SUSPENDED.equals(attrName)) {
- realActivitySuspended = Boolean.valueOf(attrValue);
- } else if (ATTR_ORIGACTIVITY.equals(attrName)) {
- origActivity = ComponentName.unflattenFromString(attrValue);
- } else if (ATTR_AFFINITY.equals(attrName)) {
- affinity = attrValue;
- } else if (ATTR_ROOT_AFFINITY.equals(attrName)) {
- rootAffinity = attrValue;
- hasRootAffinity = true;
- } else if (ATTR_ROOTHASRESET.equals(attrName)) {
- rootHasReset = Boolean.parseBoolean(attrValue);
- } else if (ATTR_AUTOREMOVERECENTS.equals(attrName)) {
- autoRemoveRecents = Boolean.parseBoolean(attrValue);
- } else if (ATTR_ASKEDCOMPATMODE.equals(attrName)) {
- askedCompatMode = Boolean.parseBoolean(attrValue);
- } else if (ATTR_USERID.equals(attrName)) {
- userId = Integer.parseInt(attrValue);
- } else if (ATTR_USER_SETUP_COMPLETE.equals(attrName)) {
- userSetupComplete = Boolean.parseBoolean(attrValue);
- } else if (ATTR_EFFECTIVE_UID.equals(attrName)) {
- effectiveUid = Integer.parseInt(attrValue);
- } else if (ATTR_TASKTYPE.equals(attrName)) {
- taskType = Integer.parseInt(attrValue);
- } else if (ATTR_LASTDESCRIPTION.equals(attrName)) {
- lastDescription = attrValue;
- } else if (ATTR_LASTTIMEMOVED.equals(attrName)) {
- lastTimeOnTop = Long.parseLong(attrValue);
- } else if (ATTR_NEVERRELINQUISH.equals(attrName)) {
- neverRelinquishIdentity = Boolean.parseBoolean(attrValue);
- } else if (attrName.startsWith(TaskDescription.ATTR_TASKDESCRIPTION_PREFIX)) {
- taskDescription.restoreFromXml(attrName, attrValue);
- } else if (ATTR_TASK_AFFILIATION.equals(attrName)) {
- taskAffiliation = Integer.parseInt(attrValue);
- } else if (ATTR_PREV_AFFILIATION.equals(attrName)) {
- prevTaskId = Integer.parseInt(attrValue);
- } else if (ATTR_NEXT_AFFILIATION.equals(attrName)) {
- nextTaskId = Integer.parseInt(attrValue);
- } else if (ATTR_TASK_AFFILIATION_COLOR.equals(attrName)) {
- taskAffiliationColor = Integer.parseInt(attrValue);
- } else if (ATTR_CALLING_UID.equals(attrName)) {
- callingUid = Integer.parseInt(attrValue);
- } else if (ATTR_CALLING_PACKAGE.equals(attrName)) {
- callingPackage = attrValue;
- } else if (ATTR_RESIZE_MODE.equals(attrName)) {
- resizeMode = Integer.parseInt(attrValue);
- } else if (ATTR_SUPPORTS_PICTURE_IN_PICTURE.equals(attrName)) {
- supportsPictureInPicture = Boolean.parseBoolean(attrValue);
- } else if (ATTR_NON_FULLSCREEN_BOUNDS.equals(attrName)) {
- bounds = Rect.unflattenFromString(attrValue);
- } else if (ATTR_MIN_WIDTH.equals(attrName)) {
- minWidth = Integer.parseInt(attrValue);
- } else if (ATTR_MIN_HEIGHT.equals(attrName)) {
- minHeight = Integer.parseInt(attrValue);
- } else if (ATTR_PERSIST_TASK_VERSION.equals(attrName)) {
- persistTaskVersion = Integer.parseInt(attrValue);
- } else {
- Slog.w(TAG, "TaskRecord: Unknown attribute=" + attrName);
- }
- }
-
- int event;
- while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
- (event != XmlPullParser.END_TAG || in.getDepth() >= outerDepth)) {
- if (event == XmlPullParser.START_TAG) {
- final String name = in.getName();
- if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, "TaskRecord: START_TAG name=" +
- name);
- if (TAG_AFFINITYINTENT.equals(name)) {
- affinityIntent = Intent.restoreFromXml(in);
- } else if (TAG_INTENT.equals(name)) {
- intent = Intent.restoreFromXml(in);
- } else if (TAG_ACTIVITY.equals(name)) {
- ActivityRecord activity = ActivityRecord.restoreFromXml(in, stackSupervisor);
- if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, "TaskRecord: activity=" +
- activity);
- if (activity != null) {
- activities.add(activity);
- }
- } else {
- Slog.e(TAG, "restoreTask: Unexpected name=" + name);
- XmlUtils.skipCurrentTag(in);
- }
- }
- }
- if (!hasRootAffinity) {
- rootAffinity = affinity;
- } else if ("@".equals(rootAffinity)) {
- rootAffinity = null;
- }
- if (effectiveUid <= 0) {
- Intent checkIntent = intent != null ? intent : affinityIntent;
- effectiveUid = 0;
- if (checkIntent != null) {
- IPackageManager pm = AppGlobals.getPackageManager();
- try {
- ApplicationInfo ai = pm.getApplicationInfo(
- checkIntent.getComponent().getPackageName(),
- PackageManager.MATCH_UNINSTALLED_PACKAGES
- | PackageManager.MATCH_DISABLED_COMPONENTS, userId);
- if (ai != null) {
- effectiveUid = ai.uid;
- }
- } catch (RemoteException e) {
- }
- }
- Slog.w(TAG, "Updating task #" + taskId + " for " + checkIntent
- + ": effectiveUid=" + effectiveUid);
- }
-
- if (persistTaskVersion < 1) {
- // We need to convert the resize mode of home activities saved before version one if
- // they are marked as RESIZE_MODE_RESIZEABLE to RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION
- // since we didn't have that differentiation before version 1 and the system didn't
- // resize home activities before then.
- if (taskType == 1 /* old home type */ && resizeMode == RESIZE_MODE_RESIZEABLE) {
- resizeMode = RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION;
- }
- } else {
- // This activity has previously marked itself explicitly as both resizeable and
- // supporting picture-in-picture. Since there is no longer a requirement for
- // picture-in-picture activities to be resizeable, we can mark this simply as
- // resizeable and supporting picture-in-picture separately.
- if (resizeMode == RESIZE_MODE_RESIZEABLE_AND_PIPABLE_DEPRECATED) {
- resizeMode = RESIZE_MODE_RESIZEABLE;
- supportsPictureInPicture = true;
- }
- }
-
- final TaskRecord task = new TaskRecord(stackSupervisor.mService, taskId, intent,
- affinityIntent, affinity, rootAffinity, realActivity, origActivity, rootHasReset,
- autoRemoveRecents, askedCompatMode, userId, effectiveUid, lastDescription,
- activities, lastTimeOnTop, neverRelinquishIdentity, taskDescription,
- taskAffiliation, prevTaskId, nextTaskId, taskAffiliationColor, callingUid,
- callingPackage, resizeMode, supportsPictureInPicture, realActivitySuspended,
- userSetupComplete, minWidth, minHeight);
- task.updateOverrideConfiguration(bounds);
-
- for (int activityNdx = activities.size() - 1; activityNdx >=0; --activityNdx) {
- activities.get(activityNdx).setTask(task);
- }
-
- if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "Restored task=" + task);
- return task;
- }
-
private void adjustForMinimalTaskDimensions(Rect bounds) {
if (bounds == null) {
return;
@@ -2320,4 +2063,388 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
top = base = null;
}
}
+
+ /**
+ * Saves this {@link TaskRecord} to XML using given serializer.
+ */
+ void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException {
+ if (DEBUG_RECENTS) Slog.i(TAG_RECENTS, "Saving task=" + this);
+
+ out.attribute(null, ATTR_TASKID, String.valueOf(taskId));
+ if (realActivity != null) {
+ out.attribute(null, ATTR_REALACTIVITY, realActivity.flattenToShortString());
+ }
+ out.attribute(null, ATTR_REALACTIVITY_SUSPENDED, String.valueOf(realActivitySuspended));
+ if (origActivity != null) {
+ out.attribute(null, ATTR_ORIGACTIVITY, origActivity.flattenToShortString());
+ }
+ // Write affinity, and root affinity if it is different from affinity.
+ // We use the special string "@" for a null root affinity, so we can identify
+ // later whether we were given a root affinity or should just make it the
+ // same as the affinity.
+ if (affinity != null) {
+ out.attribute(null, ATTR_AFFINITY, affinity);
+ if (!affinity.equals(rootAffinity)) {
+ out.attribute(null, ATTR_ROOT_AFFINITY, rootAffinity != null ? rootAffinity : "@");
+ }
+ } else if (rootAffinity != null) {
+ out.attribute(null, ATTR_ROOT_AFFINITY, rootAffinity != null ? rootAffinity : "@");
+ }
+ out.attribute(null, ATTR_ROOTHASRESET, String.valueOf(rootWasReset));
+ out.attribute(null, ATTR_AUTOREMOVERECENTS, String.valueOf(autoRemoveRecents));
+ out.attribute(null, ATTR_ASKEDCOMPATMODE, String.valueOf(askedCompatMode));
+ out.attribute(null, ATTR_USERID, String.valueOf(userId));
+ out.attribute(null, ATTR_USER_SETUP_COMPLETE, String.valueOf(mUserSetupComplete));
+ out.attribute(null, ATTR_EFFECTIVE_UID, String.valueOf(effectiveUid));
+ out.attribute(null, ATTR_LASTTIMEMOVED, String.valueOf(mLastTimeMoved));
+ out.attribute(null, ATTR_NEVERRELINQUISH, String.valueOf(mNeverRelinquishIdentity));
+ if (lastDescription != null) {
+ out.attribute(null, ATTR_LASTDESCRIPTION, lastDescription.toString());
+ }
+ if (lastTaskDescription != null) {
+ lastTaskDescription.saveToXml(out);
+ }
+ out.attribute(null, ATTR_TASK_AFFILIATION_COLOR, String.valueOf(mAffiliatedTaskColor));
+ out.attribute(null, ATTR_TASK_AFFILIATION, String.valueOf(mAffiliatedTaskId));
+ out.attribute(null, ATTR_PREV_AFFILIATION, String.valueOf(mPrevAffiliateTaskId));
+ out.attribute(null, ATTR_NEXT_AFFILIATION, String.valueOf(mNextAffiliateTaskId));
+ out.attribute(null, ATTR_CALLING_UID, String.valueOf(mCallingUid));
+ out.attribute(null, ATTR_CALLING_PACKAGE, mCallingPackage == null ? "" : mCallingPackage);
+ out.attribute(null, ATTR_RESIZE_MODE, String.valueOf(mResizeMode));
+ out.attribute(null, ATTR_SUPPORTS_PICTURE_IN_PICTURE,
+ String.valueOf(mSupportsPictureInPicture));
+ if (mLastNonFullscreenBounds != null) {
+ out.attribute(
+ null, ATTR_NON_FULLSCREEN_BOUNDS, mLastNonFullscreenBounds.flattenToString());
+ }
+ out.attribute(null, ATTR_MIN_WIDTH, String.valueOf(mMinWidth));
+ out.attribute(null, ATTR_MIN_HEIGHT, String.valueOf(mMinHeight));
+ out.attribute(null, ATTR_PERSIST_TASK_VERSION, String.valueOf(PERSIST_TASK_VERSION));
+
+ if (affinityIntent != null) {
+ out.startTag(null, TAG_AFFINITYINTENT);
+ affinityIntent.saveToXml(out);
+ out.endTag(null, TAG_AFFINITYINTENT);
+ }
+
+ out.startTag(null, TAG_INTENT);
+ intent.saveToXml(out);
+ out.endTag(null, TAG_INTENT);
+
+ final ArrayList<ActivityRecord> activities = mActivities;
+ final int numActivities = activities.size();
+ for (int activityNdx = 0; activityNdx < numActivities; ++activityNdx) {
+ final ActivityRecord r = activities.get(activityNdx);
+ if (r.info.persistableMode == ActivityInfo.PERSIST_ROOT_ONLY || !r.isPersistable() ||
+ ((r.intent.getFlags() & FLAG_ACTIVITY_NEW_DOCUMENT
+ | FLAG_ACTIVITY_RETAIN_IN_RECENTS) == FLAG_ACTIVITY_NEW_DOCUMENT) &&
+ activityNdx > 0) {
+ // Stop at first non-persistable or first break in task (CLEAR_WHEN_TASK_RESET).
+ break;
+ }
+ out.startTag(null, TAG_ACTIVITY);
+ r.saveToXml(out);
+ out.endTag(null, TAG_ACTIVITY);
+ }
+ }
+
+ @VisibleForTesting
+ static TaskRecordFactory getTaskRecordFactory() {
+ if (sTaskRecordFactory == null) {
+ setTaskRecordFactory(new TaskRecordFactory());
+ }
+ return sTaskRecordFactory;
+ }
+
+ static void setTaskRecordFactory(TaskRecordFactory factory) {
+ sTaskRecordFactory = factory;
+ }
+
+ static TaskRecord create(ActivityManagerService service, int taskId, ActivityInfo info,
+ Intent intent, IVoiceInteractionSession voiceSession,
+ IVoiceInteractor voiceInteractor) {
+ return getTaskRecordFactory().create(
+ service, taskId, info, intent, voiceSession, voiceInteractor);
+ }
+
+ static TaskRecord create(ActivityManagerService service, int taskId, ActivityInfo info,
+ Intent intent, TaskDescription taskDescription) {
+ return getTaskRecordFactory().create(service, taskId, info, intent, taskDescription);
+ }
+
+ static TaskRecord restoreFromXml(XmlPullParser in, ActivityStackSupervisor stackSupervisor)
+ throws IOException, XmlPullParserException {
+ return getTaskRecordFactory().restoreFromXml(in, stackSupervisor);
+ }
+
+ /**
+ * A factory class used to create {@link TaskRecord} or its subclass if any. This can be
+ * specified when system boots by setting it with
+ * {@link #setTaskRecordFactory(TaskRecordFactory)}.
+ */
+ static class TaskRecordFactory {
+
+ TaskRecord create(ActivityManagerService service, int taskId, ActivityInfo info,
+ Intent intent, IVoiceInteractionSession voiceSession,
+ IVoiceInteractor voiceInteractor) {
+ return new TaskRecord(
+ service, taskId, info, intent, voiceSession, voiceInteractor);
+ }
+
+ TaskRecord create(ActivityManagerService service, int taskId, ActivityInfo info,
+ Intent intent, TaskDescription taskDescription) {
+ return new TaskRecord(service, taskId, info, intent, taskDescription);
+ }
+
+ /**
+ * Should only be used when we're restoring {@link TaskRecord} from storage.
+ */
+ TaskRecord create(ActivityManagerService service, int taskId, Intent intent,
+ Intent affinityIntent, String affinity, String rootAffinity,
+ ComponentName realActivity, ComponentName origActivity, boolean rootWasReset,
+ boolean autoRemoveRecents, boolean askedCompatMode, int userId,
+ int effectiveUid, String lastDescription, ArrayList<ActivityRecord> activities,
+ long lastTimeMoved, boolean neverRelinquishIdentity,
+ TaskDescription lastTaskDescription, int taskAffiliation, int prevTaskId,
+ int nextTaskId, int taskAffiliationColor, int callingUid, String callingPackage,
+ int resizeMode, boolean supportsPictureInPicture, boolean realActivitySuspended,
+ boolean userSetupComplete, int minWidth, int minHeight) {
+ return new TaskRecord(service, taskId, intent, affinityIntent, affinity,
+ rootAffinity, realActivity, origActivity, rootWasReset, autoRemoveRecents,
+ askedCompatMode, userId, effectiveUid, lastDescription, activities,
+ lastTimeMoved, neverRelinquishIdentity, lastTaskDescription, taskAffiliation,
+ prevTaskId, nextTaskId, taskAffiliationColor, callingUid, callingPackage,
+ resizeMode, supportsPictureInPicture, realActivitySuspended, userSetupComplete,
+ minWidth, minHeight);
+ }
+
+ TaskRecord restoreFromXml(XmlPullParser in, ActivityStackSupervisor stackSupervisor)
+ throws IOException, XmlPullParserException {
+ Intent intent = null;
+ Intent affinityIntent = null;
+ ArrayList<ActivityRecord> activities = new ArrayList<>();
+ ComponentName realActivity = null;
+ boolean realActivitySuspended = false;
+ ComponentName origActivity = null;
+ String affinity = null;
+ String rootAffinity = null;
+ boolean hasRootAffinity = false;
+ boolean rootHasReset = false;
+ boolean autoRemoveRecents = false;
+ boolean askedCompatMode = false;
+ int taskType = 0;
+ int userId = 0;
+ boolean userSetupComplete = true;
+ int effectiveUid = -1;
+ String lastDescription = null;
+ long lastTimeOnTop = 0;
+ boolean neverRelinquishIdentity = true;
+ int taskId = INVALID_TASK_ID;
+ final int outerDepth = in.getDepth();
+ TaskDescription taskDescription = new TaskDescription();
+ int taskAffiliation = INVALID_TASK_ID;
+ int taskAffiliationColor = 0;
+ int prevTaskId = INVALID_TASK_ID;
+ int nextTaskId = INVALID_TASK_ID;
+ int callingUid = -1;
+ String callingPackage = "";
+ int resizeMode = RESIZE_MODE_FORCE_RESIZEABLE;
+ boolean supportsPictureInPicture = false;
+ Rect lastNonFullscreenBounds = null;
+ int minWidth = INVALID_MIN_SIZE;
+ int minHeight = INVALID_MIN_SIZE;
+ int persistTaskVersion = 0;
+
+ for (int attrNdx = in.getAttributeCount() - 1; attrNdx >= 0; --attrNdx) {
+ final String attrName = in.getAttributeName(attrNdx);
+ final String attrValue = in.getAttributeValue(attrNdx);
+ if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, "TaskRecord: attribute name=" +
+ attrName + " value=" + attrValue);
+ switch (attrName) {
+ case ATTR_TASKID:
+ if (taskId == INVALID_TASK_ID) taskId = Integer.parseInt(attrValue);
+ break;
+ case ATTR_REALACTIVITY:
+ realActivity = ComponentName.unflattenFromString(attrValue);
+ break;
+ case ATTR_REALACTIVITY_SUSPENDED:
+ realActivitySuspended = Boolean.valueOf(attrValue);
+ break;
+ case ATTR_ORIGACTIVITY:
+ origActivity = ComponentName.unflattenFromString(attrValue);
+ break;
+ case ATTR_AFFINITY:
+ affinity = attrValue;
+ break;
+ case ATTR_ROOT_AFFINITY:
+ rootAffinity = attrValue;
+ hasRootAffinity = true;
+ break;
+ case ATTR_ROOTHASRESET:
+ rootHasReset = Boolean.parseBoolean(attrValue);
+ break;
+ case ATTR_AUTOREMOVERECENTS:
+ autoRemoveRecents = Boolean.parseBoolean(attrValue);
+ break;
+ case ATTR_ASKEDCOMPATMODE:
+ askedCompatMode = Boolean.parseBoolean(attrValue);
+ break;
+ case ATTR_USERID:
+ userId = Integer.parseInt(attrValue);
+ break;
+ case ATTR_USER_SETUP_COMPLETE:
+ userSetupComplete = Boolean.parseBoolean(attrValue);
+ break;
+ case ATTR_EFFECTIVE_UID:
+ effectiveUid = Integer.parseInt(attrValue);
+ break;
+ case ATTR_TASKTYPE:
+ taskType = Integer.parseInt(attrValue);
+ break;
+ case ATTR_LASTDESCRIPTION:
+ lastDescription = attrValue;
+ break;
+ case ATTR_LASTTIMEMOVED:
+ lastTimeOnTop = Long.parseLong(attrValue);
+ break;
+ case ATTR_NEVERRELINQUISH:
+ neverRelinquishIdentity = Boolean.parseBoolean(attrValue);
+ break;
+ case ATTR_TASK_AFFILIATION:
+ taskAffiliation = Integer.parseInt(attrValue);
+ break;
+ case ATTR_PREV_AFFILIATION:
+ prevTaskId = Integer.parseInt(attrValue);
+ break;
+ case ATTR_NEXT_AFFILIATION:
+ nextTaskId = Integer.parseInt(attrValue);
+ break;
+ case ATTR_TASK_AFFILIATION_COLOR:
+ taskAffiliationColor = Integer.parseInt(attrValue);
+ break;
+ case ATTR_CALLING_UID:
+ callingUid = Integer.parseInt(attrValue);
+ break;
+ case ATTR_CALLING_PACKAGE:
+ callingPackage = attrValue;
+ break;
+ case ATTR_RESIZE_MODE:
+ resizeMode = Integer.parseInt(attrValue);
+ break;
+ case ATTR_SUPPORTS_PICTURE_IN_PICTURE:
+ supportsPictureInPicture = Boolean.parseBoolean(attrValue);
+ break;
+ case ATTR_NON_FULLSCREEN_BOUNDS:
+ lastNonFullscreenBounds = Rect.unflattenFromString(attrValue);
+ break;
+ case ATTR_MIN_WIDTH:
+ minWidth = Integer.parseInt(attrValue);
+ break;
+ case ATTR_MIN_HEIGHT:
+ minHeight = Integer.parseInt(attrValue);
+ break;
+ case ATTR_PERSIST_TASK_VERSION:
+ persistTaskVersion = Integer.parseInt(attrValue);
+ break;
+ default:
+ if (attrName.startsWith(TaskDescription.ATTR_TASKDESCRIPTION_PREFIX)) {
+ taskDescription.restoreFromXml(attrName, attrValue);
+ } else {
+ Slog.w(TAG, "TaskRecord: Unknown attribute=" + attrName);
+ }
+ }
+ }
+
+ int event;
+ while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
+ (event != XmlPullParser.END_TAG || in.getDepth() >= outerDepth)) {
+ if (event == XmlPullParser.START_TAG) {
+ final String name = in.getName();
+ if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG,
+ "TaskRecord: START_TAG name=" + name);
+ if (TAG_AFFINITYINTENT.equals(name)) {
+ affinityIntent = Intent.restoreFromXml(in);
+ } else if (TAG_INTENT.equals(name)) {
+ intent = Intent.restoreFromXml(in);
+ } else if (TAG_ACTIVITY.equals(name)) {
+ ActivityRecord activity =
+ ActivityRecord.restoreFromXml(in, stackSupervisor);
+ if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, "TaskRecord: activity=" +
+ activity);
+ if (activity != null) {
+ activities.add(activity);
+ }
+ } else {
+ handleUnknownTag(name, in);
+ }
+ }
+ }
+ if (!hasRootAffinity) {
+ rootAffinity = affinity;
+ } else if ("@".equals(rootAffinity)) {
+ rootAffinity = null;
+ }
+ if (effectiveUid <= 0) {
+ Intent checkIntent = intent != null ? intent : affinityIntent;
+ effectiveUid = 0;
+ if (checkIntent != null) {
+ IPackageManager pm = AppGlobals.getPackageManager();
+ try {
+ ApplicationInfo ai = pm.getApplicationInfo(
+ checkIntent.getComponent().getPackageName(),
+ PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DISABLED_COMPONENTS, userId);
+ if (ai != null) {
+ effectiveUid = ai.uid;
+ }
+ } catch (RemoteException e) {
+ }
+ }
+ Slog.w(TAG, "Updating task #" + taskId + " for " + checkIntent
+ + ": effectiveUid=" + effectiveUid);
+ }
+
+ if (persistTaskVersion < 1) {
+ // We need to convert the resize mode of home activities saved before version one if
+ // they are marked as RESIZE_MODE_RESIZEABLE to
+ // RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION since we didn't have that differentiation
+ // before version 1 and the system didn't resize home activities before then.
+ if (taskType == 1 /* old home type */ && resizeMode == RESIZE_MODE_RESIZEABLE) {
+ resizeMode = RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION;
+ }
+ } else {
+ // This activity has previously marked itself explicitly as both resizeable and
+ // supporting picture-in-picture. Since there is no longer a requirement for
+ // picture-in-picture activities to be resizeable, we can mark this simply as
+ // resizeable and supporting picture-in-picture separately.
+ if (resizeMode == RESIZE_MODE_RESIZEABLE_AND_PIPABLE_DEPRECATED) {
+ resizeMode = RESIZE_MODE_RESIZEABLE;
+ supportsPictureInPicture = true;
+ }
+ }
+
+ final TaskRecord task = create(stackSupervisor.mService, taskId, intent, affinityIntent,
+ affinity, rootAffinity, realActivity, origActivity, rootHasReset,
+ autoRemoveRecents, askedCompatMode, userId, effectiveUid, lastDescription,
+ activities, lastTimeOnTop, neverRelinquishIdentity, taskDescription,
+ taskAffiliation, prevTaskId, nextTaskId, taskAffiliationColor, callingUid,
+ callingPackage, resizeMode, supportsPictureInPicture, realActivitySuspended,
+ userSetupComplete, minWidth, minHeight);
+ task.mLastNonFullscreenBounds = lastNonFullscreenBounds;
+ task.setBounds(lastNonFullscreenBounds);
+
+ for (int activityNdx = activities.size() - 1; activityNdx >=0; --activityNdx) {
+ activities.get(activityNdx).setTask(task);
+ }
+
+ if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "Restored task=" + task);
+ return task;
+ }
+
+ void handleUnknownTag(String name, XmlPullParser in)
+ throws IOException, XmlPullParserException {
+ Slog.e(TAG, "restoreTask: Unexpected name=" + name);
+ XmlUtils.skipCurrentTag(in);
+ }
+ }
}
diff --git a/com/android/server/am/UserController.java b/com/android/server/am/UserController.java
index 4e3d8d27..34621e03 100644
--- a/com/android/server/am/UserController.java
+++ b/com/android/server/am/UserController.java
@@ -24,6 +24,7 @@ import static android.app.ActivityManager.USER_OP_IS_CURRENT;
import static android.app.ActivityManager.USER_OP_SUCCESS;
import static android.os.Process.SHELL_UID;
import static android.os.Process.SYSTEM_UID;
+
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -37,6 +38,7 @@ import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKED;
import static com.android.server.am.UserState.STATE_RUNNING_UNLOCKING;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.AppGlobals;
@@ -830,6 +832,9 @@ class UserController implements Handler.Callback {
private IStorageManager getStorageManager() {
return IStorageManager.Stub.asInterface(ServiceManager.getService("mount"));
}
+ boolean startUser(final int userId, final boolean foreground) {
+ return startUser(userId, foreground, null);
+ }
/**
* Start user, if its not already running.
@@ -860,7 +865,10 @@ class UserController implements Handler.Callback {
* @param foreground true if user should be brought to the foreground
* @return true if the user has been successfully started
*/
- boolean startUser(final int userId, final boolean foreground) {
+ boolean startUser(
+ final int userId,
+ final boolean foreground,
+ @Nullable IProgressListener unlockListener) {
if (mInjector.checkCallingPermission(INTERACT_ACROSS_USERS_FULL)
!= PackageManager.PERMISSION_GRANTED) {
String msg = "Permission Denial: switchUser() from pid="
@@ -919,6 +927,9 @@ class UserController implements Handler.Callback {
mUserLru.remove(userIdInt);
mUserLru.add(userIdInt);
}
+ if (unlockListener != null) {
+ uss.mUnlockProgress.addListener(unlockListener);
+ }
if (updateUmState) {
mInjector.getUserManagerInternal().setUserState(userId, uss.state);
}
@@ -1726,6 +1737,19 @@ class UserController implements Handler.Callback {
}
}
+ boolean isUserOrItsParentRunning(int userId) {
+ synchronized (mLock) {
+ if (isUserRunning(userId, 0)) {
+ return true;
+ }
+ final int parentUserId = mUserProfileGroupIds.get(userId, UserInfo.NO_PROFILE_GROUP_ID);
+ if (parentUserId == UserInfo.NO_PROFILE_GROUP_ID) {
+ return false;
+ }
+ return isUserRunning(parentUserId, 0);
+ }
+ }
+
boolean isCurrentProfile(int userId) {
synchronized (mLock) {
return ArrayUtils.contains(mCurrentProfileIds, userId);
diff --git a/com/android/server/appwidget/AppWidgetServiceImpl.java b/com/android/server/appwidget/AppWidgetServiceImpl.java
index 6c154389..54cf726c 100644
--- a/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -32,6 +32,7 @@ import android.app.PendingIntent;
import android.app.admin.DevicePolicyManagerInternal;
import android.app.admin.DevicePolicyManagerInternal.OnCrossProfileWidgetProvidersChangeListener;
import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetManagerInternal;
import android.appwidget.AppWidgetProviderInfo;
import android.appwidget.PendingHostUpdate;
import android.content.BroadcastReceiver;
@@ -100,6 +101,8 @@ import com.android.internal.appwidget.IAppWidgetHost;
import com.android.internal.appwidget.IAppWidgetService;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.CollectionUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.widget.IRemoteViewsFactory;
@@ -107,6 +110,7 @@ import com.android.server.LocalServices;
import com.android.server.WidgetBackupProvider;
import com.android.server.policy.IconUtilities;
+import libcore.util.EmptyArray;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
@@ -259,6 +263,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
computeMaximumWidgetBitmapMemory();
registerBroadcastReceiver();
registerOnCrossProfileProvidersChangedListener();
+
+ LocalServices.addService(AppWidgetManagerInternal.class, new AppWidgetManagerLocal());
}
private void computeMaximumWidgetBitmapMemory() {
@@ -2506,6 +2512,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
info.widgetCategory = sa.getInt(
com.android.internal.R.styleable.AppWidgetProviderInfo_widgetCategory,
AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN);
+ info.widgetFeatures = sa.getInt(
+ com.android.internal.R.styleable.AppWidgetProviderInfo_widgetFeatures, 0);
sa.recycle();
} catch (IOException | PackageManager.NameNotFoundException | XmlPullParserException e) {
@@ -4628,4 +4636,24 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
}
}
}
+
+ private class AppWidgetManagerLocal extends AppWidgetManagerInternal {
+ @Override
+ public ArraySet<String> getHostedWidgetPackages(int uid) {
+ synchronized (mLock) {
+ ArraySet<String> widgetPackages = null;
+ final int widgetCount = mWidgets.size();
+ for (int i = 0; i < widgetCount; i++) {
+ final Widget widget = mWidgets.get(i);
+ if (widget.host.id.uid == uid) {
+ if (widgetPackages == null) {
+ widgetPackages = new ArraySet<>();
+ }
+ widgetPackages.add(widget.provider.id.componentName.getPackageName());
+ }
+ }
+ return widgetPackages;
+ }
+ }
+ }
}
diff --git a/com/android/server/audio/AudioService.java b/com/android/server/audio/AudioService.java
index 15a418dc..799f2a92 100644
--- a/com/android/server/audio/AudioService.java
+++ b/com/android/server/audio/AudioService.java
@@ -1645,6 +1645,11 @@ public class AudioService extends IAudioService.Stub
};
private int getNewRingerMode(int stream, int index, int flags) {
+ // setRingerMode does nothing if the device is single volume,so the value would be unchanged
+ if (mIsSingleVolume) {
+ return getRingerModeExternal();
+ }
+
// setting volume on ui sounds stream type also controls silent mode
if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
(stream == getUiSoundsStreamType())) {
@@ -2198,7 +2203,7 @@ public class AudioService extends IAudioService.Stub
return (mStreamStates[streamType].getMaxIndex() + 5) / 10;
}
- /** @see AudioManager#getStreamMinVolume(int) */
+ /** @see AudioManager#getStreamMinVolumeInt(int) */
public int getStreamMinVolume(int streamType) {
ensureValidStreamType(streamType);
return (mStreamStates[streamType].getMinIndex() + 5) / 10;
@@ -2246,12 +2251,15 @@ public class AudioService extends IAudioService.Stub
if (DEBUG_VOL) {
Log.d(TAG, String.format("Mic mute %s, user=%d", on, userId));
}
- // If mute is for current user actually mute, else just persist the setting
- // which will be loaded on user switch.
+ // only mute for the current user
if (getCurrentUserId() == userId) {
+ final boolean currentMute = AudioSystem.isMicrophoneMuted();
AudioSystem.muteMicrophone(on);
+ if (on != currentMute) {
+ mContext.sendBroadcast(new Intent(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED)
+ .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY));
+ }
}
- // Post a persist microphone msg.
}
@Override
@@ -3691,7 +3699,7 @@ public class AudioService extends IAudioService.Stub
private int checkForRingerModeChange(int oldIndex, int direction, int step, boolean isMuted,
String caller, int flags) {
int result = FLAG_ADJUST_VOLUME;
- if (isPlatformTelevision()) {
+ if (isPlatformTelevision() || mIsSingleVolume) {
return result;
}
@@ -3879,7 +3887,8 @@ public class AudioService extends IAudioService.Stub
IsInCall = telecomManager.isInCall();
Binder.restoreCallingIdentity(ident);
- return (IsInCall || getMode() == AudioManager.MODE_IN_COMMUNICATION);
+ return (IsInCall || getMode() == AudioManager.MODE_IN_COMMUNICATION ||
+ getMode() == AudioManager.MODE_IN_CALL);
}
/**
diff --git a/com/android/server/autofill/AutofillManagerService.java b/com/android/server/autofill/AutofillManagerService.java
index 23e4f504..e1cb154c 100644
--- a/com/android/server/autofill/AutofillManagerService.java
+++ b/com/android/server/autofill/AutofillManagerService.java
@@ -52,6 +52,7 @@ import android.os.UserManager;
import android.os.UserManagerInternal;
import android.provider.Settings;
import android.service.autofill.FillEventHistory;
+import android.service.autofill.UserData;
import android.util.LocalLog;
import android.util.Slog;
import android.util.SparseArray;
@@ -567,13 +568,66 @@ public final class AutofillManagerService extends SystemService {
@Override
public FillEventHistory getFillEventHistory() throws RemoteException {
- UserHandle user = getCallingUserHandle();
- int uid = getCallingUid();
+ final int userId = UserHandle.getCallingUserId();
synchronized (mLock) {
- AutofillManagerServiceImpl service = peekServiceForUserLocked(user.getIdentifier());
+ final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ return service.getFillEventHistory(getCallingUid());
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public UserData getUserData() throws RemoteException {
+ final int userId = UserHandle.getCallingUserId();
+
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ return service.getUserData(getCallingUid());
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void setUserData(UserData userData) throws RemoteException {
+ final int userId = UserHandle.getCallingUserId();
+
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ service.setUserData(getCallingUid(), userData);
+ }
+ }
+ }
+
+ @Override
+ public boolean isFieldClassificationEnabled() throws RemoteException {
+ final int userId = UserHandle.getCallingUserId();
+
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ return service.isFieldClassificationEnabled(getCallingUid());
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public ComponentName getAutofillServiceComponentName() throws RemoteException {
+ final int userId = UserHandle.getCallingUserId();
+
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
- return service.getFillEventHistory(uid);
+ return service.getServiceComponentName();
}
}
@@ -723,6 +777,7 @@ public final class AutofillManagerService extends SystemService {
}
boolean oldDebug = sDebug;
+ final String prefix = " ";
try {
synchronized (mLock) {
oldDebug = sDebug;
@@ -731,6 +786,7 @@ public final class AutofillManagerService extends SystemService {
pw.print("Verbose mode: "); pw.println(sVerbose);
pw.print("Disabled users: "); pw.println(mDisabledUsers);
pw.print("Max partitions per session: "); pw.println(sPartitionMaxCount);
+ pw.println("User data constraints: "); UserData.dumpConstraints(prefix, pw);
final int size = mServicesCache.size();
pw.print("Cached services: ");
if (size == 0) {
@@ -740,7 +796,7 @@ public final class AutofillManagerService extends SystemService {
for (int i = 0; i < size; i++) {
pw.print("\nService at index "); pw.println(i);
final AutofillManagerServiceImpl impl = mServicesCache.valueAt(i);
- impl.dumpLocked(" ", pw);
+ impl.dumpLocked(prefix, pw);
}
}
mUi.dump(pw);
diff --git a/com/android/server/autofill/AutofillManagerServiceImpl.java b/com/android/server/autofill/AutofillManagerServiceImpl.java
index 21e27220..4cdfd625 100644
--- a/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -26,15 +26,18 @@ import static com.android.server.autofill.Helper.sVerbose;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
import android.app.IActivityManager;
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.ServiceInfo;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.metrics.LogMaker;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Bundle;
@@ -43,14 +46,18 @@ import android.os.Looper;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.service.autofill.AutofillService;
import android.service.autofill.AutofillServiceInfo;
+import android.service.autofill.FieldClassification;
+import android.service.autofill.FieldClassification.Match;
import android.service.autofill.FillEventHistory;
import android.service.autofill.FillEventHistory.Event;
import android.service.autofill.FillResponse;
import android.service.autofill.IAutoFillService;
+import android.service.autofill.UserData;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -69,10 +76,12 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.os.HandlerCaller;
+import com.android.server.LocalServices;
import com.android.server.autofill.ui.AutoFillUI;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.List;
import java.util.Random;
/**
@@ -121,6 +130,11 @@ final class AutofillManagerServiceImpl {
private boolean mDisabled;
/**
+ * Data used for field classification.
+ */
+ private UserData mUserData;
+
+ /**
* Caches whether the setup completed for the current user.
*/
@GuardedBy("mLock")
@@ -183,6 +197,14 @@ final class AutofillManagerServiceImpl {
}
}
+ private int getServiceUidLocked() {
+ if (mInfo == null) {
+ Slog.w(TAG, "getServiceUidLocked(): no mInfo");
+ return -1;
+ }
+ return mInfo.getServiceInfo().applicationInfo.uid;
+ }
+
@Nullable
String getServicePackageName() {
final ComponentName serviceComponent = getServiceComponentName();
@@ -334,7 +356,7 @@ final class AutofillManagerServiceImpl {
pruneAbandonedSessionsLocked();
final Session newSession = createSessionByTokenLocked(activityToken, uid, appCallbackToken,
- hasCallback, componentName);
+ hasCallback, componentName, flags);
if (newSession == null) {
return NO_SESSION;
}
@@ -378,7 +400,7 @@ final class AutofillManagerServiceImpl {
return;
}
- session.logContextCommittedLocked();
+ session.logContextCommitted();
final boolean finished = session.showSaveLocked();
if (sVerbose) Slog.v(TAG, "finishSessionLocked(): session finished on save? " + finished);
@@ -435,7 +457,7 @@ final class AutofillManagerServiceImpl {
private Session createSessionByTokenLocked(@NonNull IBinder activityToken, int uid,
@NonNull IBinder appCallbackToken, boolean hasCallback,
- @NonNull ComponentName componentName) {
+ @NonNull ComponentName componentName, int flags) {
// use random ids so that one app cannot know that another app creates sessions
int sessionId;
int tries = 0;
@@ -449,15 +471,46 @@ final class AutofillManagerServiceImpl {
sessionId = sRandom.nextInt();
} while (sessionId == NO_SESSION || mSessions.indexOfKey(sessionId) >= 0);
+ assertCallerLocked(componentName);
+
final Session newSession = new Session(this, mUi, mContext, mHandlerCaller, mUserId, mLock,
sessionId, uid, activityToken, appCallbackToken, hasCallback,
- mUiLatencyHistory, mInfo.getServiceInfo().getComponentName(), componentName);
+ mUiLatencyHistory, mInfo.getServiceInfo().getComponentName(), componentName, flags);
mSessions.put(newSession.id, newSession);
return newSession;
}
/**
+ * Asserts the component is owned by the caller.
+ */
+ private void assertCallerLocked(@NonNull ComponentName componentName) {
+ final String packageName = componentName.getPackageName();
+ final PackageManager pm = mContext.getPackageManager();
+ final int callingUid = Binder.getCallingUid();
+ final int packageUid;
+ try {
+ packageUid = pm.getPackageUidAsUser(packageName, UserHandle.getCallingUserId());
+ } catch (NameNotFoundException e) {
+ throw new SecurityException("Could not verify UID for " + componentName);
+ }
+ if (callingUid != packageUid && !LocalServices.getService(ActivityManagerInternal.class)
+ .hasRunningActivity(callingUid, packageName)) {
+ final String[] packages = pm.getPackagesForUid(callingUid);
+ final String callingPackage = packages != null ? packages[0] : "uid-" + callingUid;
+ Slog.w(TAG, "App (package=" + callingPackage + ", UID=" + callingUid
+ + ") passed component (" + componentName + ") owned by UID " + packageUid);
+ mMetricsLogger.write(
+ Helper.newLogMaker(MetricsEvent.AUTOFILL_FORGED_COMPONENT_ATTEMPT,
+ callingPackage, getServicePackageName())
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FORGED_COMPONENT_NAME,
+ componentName == null ? "null" : componentName.flattenToShortString()));
+
+ throw new SecurityException("Invalid component: " + componentName);
+ }
+ }
+
+ /**
* Restores a session after an activity was temporarily destroyed.
*
* @param sessionId The id of the session to restore
@@ -574,9 +627,9 @@ final class AutofillManagerServiceImpl {
* Initializes the last fill selection after an autofill service returned a new
* {@link FillResponse}.
*/
- void setLastResponse(int serviceUid, int sessionId, @NonNull FillResponse response) {
+ void setLastResponse(int sessionId, @NonNull FillResponse response) {
synchronized (mLock) {
- mEventHistory = new FillEventHistory(serviceUid, sessionId, response.getClientState());
+ mEventHistory = new FillEventHistory(sessionId, response.getClientState());
}
}
@@ -612,7 +665,7 @@ final class AutofillManagerServiceImpl {
if (isValidEventLocked("setAuthenticationSelected()", sessionId)) {
mEventHistory.addEvent(
new Event(Event.TYPE_AUTHENTICATION_SELECTED, null, clientState, null, null,
- null, null, null, null, null, -1));
+ null, null, null, null, null, null));
}
}
}
@@ -626,7 +679,7 @@ final class AutofillManagerServiceImpl {
if (isValidEventLocked("logDatasetAuthenticationSelected()", sessionId)) {
mEventHistory.addEvent(
new Event(Event.TYPE_DATASET_AUTHENTICATION_SELECTED, selectedDataset,
- clientState, null, null, null, null, null, null, null, -1));
+ clientState, null, null, null, null, null, null, null, null));
}
}
}
@@ -638,7 +691,7 @@ final class AutofillManagerServiceImpl {
synchronized (mLock) {
if (isValidEventLocked("logSaveShown()", sessionId)) {
mEventHistory.addEvent(new Event(Event.TYPE_SAVE_SHOWN, null, clientState, null,
- null, null, null, null, null, null, -1));
+ null, null, null, null, null, null, null));
}
}
}
@@ -652,7 +705,7 @@ final class AutofillManagerServiceImpl {
if (isValidEventLocked("logDatasetSelected()", sessionId)) {
mEventHistory.addEvent(
new Event(Event.TYPE_DATASET_SELECTED, selectedDataset, clientState, null,
- null, null, null, null, null, null, -1));
+ null, null, null, null, null, null, null));
}
}
}
@@ -660,22 +713,52 @@ final class AutofillManagerServiceImpl {
/**
* Updates the last fill response when an autofill context is committed.
*/
- void logContextCommitted(int sessionId, @Nullable Bundle clientState,
+ void logContextCommittedLocked(int sessionId, @Nullable Bundle clientState,
@Nullable ArrayList<String> selectedDatasets,
@Nullable ArraySet<String> ignoredDatasets,
@Nullable ArrayList<AutofillId> changedFieldIds,
@Nullable ArrayList<String> changedDatasetIds,
@Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
@Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
- @Nullable String detectedRemoteId, int detectedFieldScore) {
- synchronized (mLock) {
- if (isValidEventLocked("logDatasetNotSelected()", sessionId)) {
- mEventHistory.addEvent(new Event(Event.TYPE_CONTEXT_COMMITTED, null,
- clientState, selectedDatasets, ignoredDatasets,
- changedFieldIds, changedDatasetIds,
- manuallyFilledFieldIds, manuallyFilledDatasetIds,
- detectedRemoteId, detectedFieldScore));
+ @Nullable ArrayList<AutofillId> detectedFieldIdsList,
+ @Nullable ArrayList<FieldClassification> detectedFieldClassificationsList,
+ @NonNull String appPackageName) {
+ if (isValidEventLocked("logDatasetNotSelected()", sessionId)) {
+ AutofillId[] detectedFieldsIds = null;
+ FieldClassification[] detectedFieldClassifications = null;
+ if (detectedFieldIdsList != null) {
+ detectedFieldsIds = new AutofillId[detectedFieldIdsList.size()];
+ detectedFieldIdsList.toArray(detectedFieldsIds);
+ detectedFieldClassifications =
+ new FieldClassification[detectedFieldClassificationsList.size()];
+ detectedFieldClassificationsList.toArray(detectedFieldClassifications);
+
+ final int numberFields = detectedFieldsIds.length;
+ int totalSize = 0;
+ float totalScore = 0;
+ for (int i = 0; i < numberFields; i++) {
+ final FieldClassification fc = detectedFieldClassifications[i];
+ final List<Match> matches = fc.getMatches();
+ final int size = matches.size();
+ totalSize += size;
+ for (int j = 0; j < size; j++) {
+ totalScore += matches.get(j).getScore();
+ }
+ }
+
+ final int averageScore = (int) ((totalScore * 100) / totalSize);
+ mMetricsLogger.write(Helper
+ .newLogMaker(MetricsEvent.AUTOFILL_FIELD_CLASSIFICATION_MATCHES,
+ appPackageName, getServicePackageName())
+ .setCounterValue(numberFields)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_MATCH_SCORE,
+ averageScore));
}
+ mEventHistory.addEvent(new Event(Event.TYPE_CONTEXT_COMMITTED, null,
+ clientState, selectedDatasets, ignoredDatasets,
+ changedFieldIds, changedDatasetIds,
+ manuallyFilledFieldIds, manuallyFilledDatasetIds,
+ detectedFieldsIds, detectedFieldClassifications));
}
}
@@ -688,18 +771,59 @@ final class AutofillManagerServiceImpl {
*/
FillEventHistory getFillEventHistory(int callingUid) {
synchronized (mLock) {
- if (mEventHistory != null && mEventHistory.getServiceUid() == callingUid) {
+ if (mEventHistory != null
+ && isCalledByServiceLocked("getFillEventHistory", callingUid)) {
return mEventHistory;
}
}
+ return null;
+ }
+
+ // Called by Session - does not need to check uid
+ UserData getUserData() {
+ synchronized (mLock) {
+ return mUserData;
+ }
+ }
+ // Called by AutofillManager
+ UserData getUserData(int callingUid) {
+ synchronized (mLock) {
+ if (isCalledByServiceLocked("getUserData", callingUid)) {
+ return mUserData;
+ }
+ }
return null;
}
+ // Called by AutofillManager
+ void setUserData(int callingUid, UserData userData) {
+ synchronized (mLock) {
+ if (isCalledByServiceLocked("setUserData", callingUid)) {
+ mUserData = userData;
+ // Log it
+ int numberFields = mUserData == null ? 0: mUserData.getRemoteIds().length;
+ mMetricsLogger.write(Helper.newLogMaker(MetricsEvent.AUTOFILL_USERDATA_UPDATED,
+ getServicePackageName(), null)
+ .setCounterValue(numberFields));
+ }
+ }
+ }
+
+ private boolean isCalledByServiceLocked(String methodName, int callingUid) {
+ if (getServiceUidLocked() != callingUid) {
+ Slog.w(TAG, methodName + "() called by UID " + callingUid
+ + ", but service UID is " + getServiceUidLocked());
+ return false;
+ }
+ return true;
+ }
+
void dumpLocked(String prefix, PrintWriter pw) {
final String prefix2 = prefix + " ";
pw.print(prefix); pw.print("User: "); pw.println(mUserId);
+ pw.print(prefix); pw.print("UID: "); pw.println(getServiceUidLocked());
pw.print(prefix); pw.print("Component: "); pw.println(mInfo != null
? mInfo.getServiceInfo().getComponentName() : null);
pw.print(prefix); pw.print("Component from settings: ");
@@ -707,7 +831,8 @@ final class AutofillManagerServiceImpl {
pw.print(prefix); pw.print("Default component: ");
pw.println(mContext.getString(R.string.config_defaultAutofillService));
pw.print(prefix); pw.print("Disabled: "); pw.println(mDisabled);
- pw.print(prefix); pw.print("Field detection: "); pw.println(isFieldDetectionEnabled());
+ pw.print(prefix); pw.print("Field classification enabled: ");
+ pw.println(isFieldClassificationEnabledLocked());
pw.print(prefix); pw.print("Setup complete: "); pw.println(mSetupComplete);
pw.print(prefix); pw.print("Last prune: "); pw.println(mLastPrune);
@@ -762,8 +887,13 @@ final class AutofillManagerServiceImpl {
}
}
- pw.print(prefix); pw.println("Clients");
- mClients.dump(pw, prefix2);
+ pw.print(prefix); pw.print("Clients: ");
+ if (mClients == null) {
+ pw.println("N/A");
+ } else {
+ pw.println();
+ mClients.dump(pw, prefix2);
+ }
if (mEventHistory == null || mEventHistory.getEvents() == null
|| mEventHistory.getEvents().size() == 0) {
@@ -779,6 +909,14 @@ final class AutofillManagerServiceImpl {
+ event.getDatasetId());
}
}
+
+ pw.print(prefix); pw.print("User data: ");
+ if (mUserData == null) {
+ pw.println("N/A");
+ } else {
+ pw.println();
+ mUserData.dump(prefix2, pw);
+ }
}
void destroySessionsLocked() {
@@ -905,10 +1043,12 @@ final class AutofillManagerServiceImpl {
expiration = Long.MAX_VALUE;
}
mDisabledActivities.put(componentName, expiration);
- int intDuration = duration > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) duration;
- mMetricsLogger.write(Helper.newLogMaker(MetricsEvent.AUTOFILL_SERVICE_DISABLED_ACTIVITY,
- componentName.getPackageName(), getServicePackageName())
- .addTaggedData(MetricsEvent.FIELD_CLASS_NAME, componentName.getClassName())
+ final int intDuration = duration > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) duration;
+ // NOTE: not using Helper.newLogMaker() because we're setting the componentName instead
+ // of package name
+ mMetricsLogger.write(new LogMaker(MetricsEvent.AUTOFILL_SERVICE_DISABLED_ACTIVITY)
+ .setComponentName(componentName)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, getServicePackageName())
.setCounterValue(intDuration));
}
}
@@ -926,7 +1066,8 @@ final class AutofillManagerServiceImpl {
if (expiration >= elapsedTime) return true;
// Restriction expired - clean it up.
if (sVerbose) {
- Slog.v(TAG, "Removing " + componentName.toShortString() + " from disabled list");
+ Slog.v(TAG, "Removing " + componentName.toShortString()
+ + " from disabled list");
}
mDisabledActivities.remove(componentName);
}
@@ -951,10 +1092,21 @@ final class AutofillManagerServiceImpl {
return false;
}
- // TODO(b/67867469): remove once feature is finished
- boolean isFieldDetectionEnabled() {
+ // Called by AutofillManager, checks UID.
+ boolean isFieldClassificationEnabled(int uid) {
+ synchronized (mLock) {
+ if (!isCalledByServiceLocked("isFieldClassificationEnabled", uid)) {
+ return false;
+ }
+ return isFieldClassificationEnabledLocked();
+ }
+ }
+
+ // Called by internally, no need to check UID.
+ boolean isFieldClassificationEnabledLocked() {
return Settings.Secure.getIntForUser(
- mContext.getContentResolver(), Settings.Secure.AUTOFILL_FEATURE_FIELD_DETECTION, 0,
+ mContext.getContentResolver(),
+ Settings.Secure.AUTOFILL_FEATURE_FIELD_CLASSIFICATION, 0,
mUserId) == 1;
}
diff --git a/com/android/server/autofill/RemoteFillService.java b/com/android/server/autofill/RemoteFillService.java
index 831c488e..aea9ad0e 100644
--- a/com/android/server/autofill/RemoteFillService.java
+++ b/com/android/server/autofill/RemoteFillService.java
@@ -97,7 +97,7 @@ final class RemoteFillService implements DeathRecipient {
private PendingRequest mPendingRequest;
public interface FillServiceCallbacks {
- void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response, int serviceUid,
+ void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response,
@NonNull String servicePackageName);
void onFillRequestFailure(@Nullable CharSequence message,
@NonNull String servicePackageName);
@@ -281,11 +281,11 @@ final class RemoteFillService implements DeathRecipient {
mContext.unbindService(mServiceConnection);
}
- private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest,
- int callingUid, int requestFlags, FillResponse response) {
+ private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest, int requestFlags,
+ FillResponse response) {
mHandler.getHandler().post(() -> {
if (handleResponseCallbackCommon(pendingRequest)) {
- mCallbacks.onFillRequestSuccess(requestFlags, response, callingUid,
+ mCallbacks.onFillRequestSuccess(requestFlags, response,
mComponentName.getPackageName());
}
});
@@ -546,7 +546,7 @@ final class RemoteFillService implements DeathRecipient {
final RemoteFillService remoteService = getService();
if (remoteService != null) {
remoteService.dispatchOnFillRequestSuccess(PendingFillRequest.this,
- getCallingUid(), request.getFlags(), response);
+ request.getFlags(), response);
}
}
diff --git a/com/android/server/autofill/Session.java b/com/android/server/autofill/Session.java
index 99b92b9c..01f90840 100644
--- a/com/android/server/autofill/Session.java
+++ b/com/android/server/autofill/Session.java
@@ -55,7 +55,7 @@ import android.os.RemoteException;
import android.os.SystemClock;
import android.service.autofill.AutofillService;
import android.service.autofill.Dataset;
-import android.service.autofill.FieldsDetection;
+import android.service.autofill.FieldClassification.Match;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
@@ -63,7 +63,9 @@ import android.service.autofill.InternalSanitizer;
import android.service.autofill.InternalValidator;
import android.service.autofill.SaveInfo;
import android.service.autofill.SaveRequest;
+import android.service.autofill.UserData;
import android.service.autofill.ValueFinder;
+import android.service.autofill.FieldClassification;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.LocalLog;
@@ -126,6 +128,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
/** uid the session is for */
public final int uid;
+ /** Flags used to start the session */
+ public final int mFlags;
+
@GuardedBy("mLock")
@NonNull private IBinder mActivityToken;
@@ -236,6 +241,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
structure.ensureData();
// Sanitize structure before it's sent to service.
+ final ComponentName componentNameFromApp = structure.getActivityComponent();
+ if (!mComponentName.equals(componentNameFromApp)) {
+ Slog.w(TAG, "Activity " + mComponentName + " forged different component on "
+ + "AssistStructure: " + componentNameFromApp);
+ structure.setActivityComponent(mComponentName);
+ mMetricsLogger.write(newLogMaker(MetricsEvent.AUTOFILL_FORGED_COMPONENT_ATTEMPT)
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_FORGED_COMPONENT_NAME,
+ componentNameFromApp == null ? "null"
+ : componentNameFromApp.flattenToShortString()));
+ }
structure.sanitizeForParceling(true);
// Flags used to start the session.
@@ -429,8 +444,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
@NonNull Context context, @NonNull HandlerCaller handlerCaller, int userId,
@NonNull Object lock, int sessionId, int uid, @NonNull IBinder activityToken,
@NonNull IBinder client, boolean hasCallback, @NonNull LocalLog uiLatencyHistory,
- @NonNull ComponentName serviceComponentName, @NonNull ComponentName componentName) {
+ @NonNull ComponentName serviceComponentName, @NonNull ComponentName componentName,
+ int flags) {
id = sessionId;
+ mFlags = flags;
this.uid = uid;
mStartTime = SystemClock.elapsedRealtime();
mService = service;
@@ -444,7 +461,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mComponentName = componentName;
mClient = IAutoFillManagerClient.Stub.asInterface(client);
- writeLog(MetricsEvent.AUTOFILL_SESSION_STARTED);
+ mMetricsLogger.write(newLogMaker(MetricsEvent.AUTOFILL_SESSION_STARTED)
+ .addTaggedData(MetricsEvent.FIELD_FLAGS, flags));
}
/**
@@ -480,7 +498,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
// FillServiceCallbacks
@Override
public void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response,
- int serviceUid, @NonNull String servicePackageName) {
+ @NonNull String servicePackageName) {
+ final AutofillId[] fieldClassificationIds;
+
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onFillRequestSuccess() rejected - session: "
@@ -491,16 +511,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
processNullResponseLocked(requestFlags);
return;
}
- }
- // TODO(b/67867469): remove once feature is finished
- if (response.getFieldsDetection() != null && !mService.isFieldDetectionEnabled()) {
- Slog.w(TAG, "Ignoring " + response + " because field detection is disabled");
- processNullResponseLocked(requestFlags);
- return;
+ fieldClassificationIds = response.getFieldClassificationIds();
+ if (fieldClassificationIds != null && !mService.isFieldClassificationEnabledLocked()) {
+ Slog.w(TAG, "Ignoring " + response + " because field detection is disabled");
+ processNullResponseLocked(requestFlags);
+ return;
+ }
}
- mService.setLastResponse(serviceUid, id, response);
+ mService.setLastResponse(id, response);
int sessionFinishedState = 0;
final long disableDuration = response.getDisableDuration();
@@ -536,6 +556,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
.setType(MetricsEvent.TYPE_SUCCESS)
.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS,
response.getDatasets() == null ? 0 : response.getDatasets().size());
+ if (fieldClassificationIds != null) {
+ log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_FIELD_CLASSIFICATION_IDS,
+ fieldClassificationIds.length);
+ }
mMetricsLogger.write(log);
}
@@ -882,7 +906,15 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
* Generates a {@link android.service.autofill.FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}
* when necessary.
*/
- public void logContextCommittedLocked() {
+ public void logContextCommitted() {
+ mHandlerCaller.getHandler().post(() -> {
+ synchronized (mLock) {
+ logContextCommittedLocked();
+ }
+ });
+ }
+
+ private void logContextCommittedLocked() {
final FillResponse lastResponse = getLastResponseLocked("logContextCommited()");
if (lastResponse == null) return;
@@ -903,7 +935,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final FillResponse response = mResponses.valueAt(i);
final List<Dataset> datasets = response.getDatasets();
if (datasets == null || datasets.isEmpty()) {
- if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i);
+ if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i);
} else {
for (int j = 0; j < datasets.size(); j++) {
final Dataset dataset = datasets.get(j);
@@ -926,29 +958,30 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
}
}
- final FieldsDetection fieldsDetection = lastResponse.getFieldsDetection();
+ final AutofillId[] fieldClassificationIds = lastResponse.getFieldClassificationIds();
- if (!hasAtLeastOneDataset && fieldsDetection == null) {
+ if (!hasAtLeastOneDataset && fieldClassificationIds == null) {
if (sVerbose) {
Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields "
- + "detection)");
+ + "classification ids)");
}
return;
}
- final AutofillId detectableFieldId;
- final String detectableRemoteId;
- String detectedRemoteId = null;
- if (fieldsDetection == null) {
- detectableFieldId = null;
- detectableRemoteId = null;
+ final UserData userData = mService.getUserData();
+
+ final ArrayList<AutofillId> detectedFieldIds;
+ final ArrayList<FieldClassification> detectedFieldClassifications;
+
+ if (userData != null) {
+ final int maxFieldsSize = UserData.getMaxFieldClassificationIdsSize();
+ detectedFieldIds = new ArrayList<>(maxFieldsSize);
+ detectedFieldClassifications = new ArrayList<>(maxFieldsSize);
} else {
- detectableFieldId = fieldsDetection.getFieldId();
- detectableRemoteId = fieldsDetection.getRemoteId();
+ detectedFieldIds = null;
+ detectedFieldClassifications = null;
}
- int detectedFieldScore = -1;
-
for (int i = 0; i < mViewStates.size(); i++) {
final ViewState viewState = mViewStates.valueAt(i);
final int state = viewState.getState();
@@ -991,8 +1024,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final AutofillValue currentValue = viewState.getCurrentValue();
if (currentValue == null) {
if (sDebug) {
- Slog.d(TAG, "logContextCommitted(): skipping view witout current value "
- + "( " + viewState + ")");
+ Slog.d(TAG, "logContextCommitted(): skipping view without current "
+ + "value ( " + viewState + ")");
}
continue;
}
@@ -1053,18 +1086,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
} // for j
}
- // Check if detectable field changed.
- if (detectableFieldId != null && detectableFieldId.equals(viewState.id)
- && currentValue.isText() && currentValue.getTextValue() != null) {
- final String actualValue = currentValue.getTextValue().toString();
- final String expectedValue = fieldsDetection.getValue();
- if (actualValue.equalsIgnoreCase(expectedValue)) {
- detectedRemoteId = detectableRemoteId;
- detectedFieldScore = 0;
- } else if (sVerbose) {
- Slog.v(TAG, "Detection mismatch for field " + detectableFieldId);
- }
- // TODO(b/67867469): set score on partial hits
+ // Sets field classification score for field
+ if (userData!= null) {
+ setScore(detectedFieldIds, detectedFieldClassifications, userData,
+ viewState.id, currentValue);
}
} // else
} // else
@@ -1077,8 +1102,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
+ ", changedAutofillIds=" + changedFieldIds
+ ", changedDatasetIds=" + changedDatasetIds
+ ", manuallyFilledIds=" + manuallyFilledIds
- + ", detectableFieldId=" + detectableFieldId
- + ", detectedFieldScore=" + detectedFieldScore
+ + ", detectedFieldIds=" + detectedFieldIds
+ + ", detectedFieldClassifications=" + detectedFieldClassifications
);
}
@@ -1098,10 +1123,53 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
}
- mService.logContextCommitted(id, mClientState, mSelectedDatasetIds, ignoredDatasets,
+ mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds, ignoredDatasets,
changedFieldIds, changedDatasetIds,
manuallyFilledFieldIds, manuallyFilledDatasetIds,
- detectedRemoteId, detectedFieldScore);
+ detectedFieldIds, detectedFieldClassifications, mComponentName.getPackageName());
+ }
+
+ /**
+ * Adds the matches to {@code detectedFieldsIds} and {@code detectedFieldClassifications} for
+ * {@code fieldId} based on its {@code currentValue} and {@code userData}.
+ */
+ private static void setScore(@NonNull ArrayList<AutofillId> detectedFieldIds,
+ @NonNull ArrayList<FieldClassification> detectedFieldClassifications,
+ @NonNull UserData userData, @NonNull AutofillId fieldId,
+ @NonNull AutofillValue currentValue) {
+
+ final String[] userValues = userData.getValues();
+ final String[] remoteIds = userData.getRemoteIds();
+
+ // Sanity check
+ if (userValues == null || remoteIds == null || userValues.length != remoteIds.length) {
+ final int valuesLength = userValues == null ? -1 : userValues.length;
+ final int idsLength = remoteIds == null ? -1 : remoteIds.length;
+ Slog.w(TAG, "setScores(): user data mismatch: values.length = "
+ + valuesLength + ", ids.length = " + idsLength);
+ return;
+ }
+
+ ArrayList<Match> matches = null;
+ for (int i = 0; i < userValues.length; i++) {
+ String remoteId = remoteIds[i];
+ final String value = userValues[i];
+ final float score = userData.getScorer().getScore(currentValue, value);
+ if (score > 0) {
+ if (sVerbose) {
+ Slog.v(TAG, "adding score " + score + " at index " + i + " and id " + fieldId);
+ }
+ if (matches == null) {
+ matches = new ArrayList<>(userValues.length);
+ }
+ matches.add(new Match(remoteId, score));
+ }
+ else if (sVerbose) Slog.v(TAG, "skipping score 0 at index " + i + " and id " + fieldId);
+ }
+ if (matches != null) {
+ detectedFieldIds.add(fieldId);
+ detectedFieldClassifications.add(new FieldClassification(matches));
+ }
}
/**
@@ -1299,7 +1367,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
if (sDebug) Slog.d(TAG, "Good news, everyone! All checks passed, show save UI!");
- mService.logSaveShown(id, mClientState);
+
+ // Use handler so logContextCommitted() is logged first
+ mHandlerCaller.getHandler().post(() -> mService.logSaveShown(id, mClientState));
+
final IAutoFillManagerClient client = getClient();
mPendingSaveUi = new PendingUi(mActivityToken, id, client);
getUiForShowing().showSaveUi(mService.getServiceLabel(), mService.getServiceIcon(),
@@ -1499,7 +1570,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
*
* <p>A new request will be started in 2 scenarios:
* <ol>
- * <li>If the user manually requested autofill after the view was already filled.
+ * <li>If the user manually requested autofill.
* <li>If the view is part of a new partition.
* </ol>
*
@@ -1507,14 +1578,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
* @param viewState The view that is entered.
* @param flags The flag that was passed by the AutofillManager.
*/
- private void requestNewFillResponseIfNecessaryLocked(@NonNull AutofillId id,
+ private void requestNewFillResponseOnViewEnteredIfNecessaryLocked(@NonNull AutofillId id,
@NonNull ViewState viewState, int flags) {
- // First check if this is a manual request after view was autofilled.
- final int state = viewState.getState();
- final boolean restart = (state & STATE_AUTOFILLED) != 0
- && (flags & FLAG_MANUAL_REQUEST) != 0;
- if (restart) {
- if (sDebug) Slog.d(TAG, "Re-starting session on view " + id);
+ if ((flags & FLAG_MANUAL_REQUEST) != 0) {
+ if (sDebug) Slog.d(TAG, "Re-starting session on view " + id + " and flags " + flags);
viewState.setState(STATE_RESTARTED_SESSION);
requestNewFillResponseLocked(flags);
return;
@@ -1607,7 +1674,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
isIgnored ? ViewState.STATE_IGNORED : ViewState.STATE_INITIAL);
mViewStates.put(id, viewState);
- // TODO(b/67867469): for optimization purposes, should also ignore if change is
+ // TODO(b/70407264): for optimization purposes, should also ignore if change is
// detectable, and batch-send them when the session is finished (but that will
// require tracking detectable fields on AutofillManager)
if (isIgnored) {
@@ -1667,9 +1734,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
break;
case ACTION_VIEW_ENTERED:
if (sVerbose && virtualBounds != null) {
- Slog.w(TAG, "entered on virtual child " + id + ": " + virtualBounds);
+ Slog.v(TAG, "entered on virtual child " + id + ": " + virtualBounds);
}
- requestNewFillResponseIfNecessaryLocked(id, viewState, flags);
+ requestNewFillResponseOnViewEnteredIfNecessaryLocked(id, viewState, flags);
// Remove the UI if the ViewState has changed.
if (mCurrentViewId != viewState.id) {
@@ -2071,6 +2138,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final String prefix2 = prefix + " ";
pw.print(prefix); pw.print("id: "); pw.println(id);
pw.print(prefix); pw.print("uid: "); pw.println(uid);
+ pw.print(prefix); pw.print("flags: "); pw.println(mFlags);
pw.print(prefix); pw.print("mComponentName: "); pw.println(mComponentName);
pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken);
pw.print(prefix); pw.print("mStartTime: "); pw.println(mStartTime);
diff --git a/com/android/server/autofill/ui/FillUi.java b/com/android/server/autofill/ui/FillUi.java
index dac4586f..5ee3cbfe 100644
--- a/com/android/server/autofill/ui/FillUi.java
+++ b/com/android/server/autofill/ui/FillUi.java
@@ -108,9 +108,11 @@ final class FillUi {
mCallback = callback;
final LayoutInflater inflater = LayoutInflater.from(context);
+
final ViewGroup decor = (ViewGroup) inflater.inflate(
R.layout.autofill_dataset_picker, null);
+
final RemoteViews.OnClickHandler interceptionHandler = new RemoteViews.OnClickHandler() {
@Override
public boolean onClickHandler(View view, PendingIntent pendingIntent,
@@ -153,7 +155,38 @@ final class FillUi {
mCallback.requestShowFillUi(mContentWidth, mContentHeight, mWindowPresenter);
} else {
final int datasetCount = response.getDatasets().size();
- final ArrayList<ViewItem> items = new ArrayList<>(datasetCount);
+
+ // Total items include the (optional) header and footer - we cannot use listview's
+ // addHeader() and addFooter() because it would complicate the scrolling logic.
+ int totalItems = datasetCount;
+
+ RemoteViews.OnClickHandler clickBlocker = null;
+ final RemoteViews headerPresentation = response.getHeader();
+ View header = null;
+ if (headerPresentation != null) {
+ clickBlocker = newClickBlocker();
+ header = headerPresentation.apply(context, null, clickBlocker);
+ totalItems++;
+ }
+
+ final RemoteViews footerPresentation = response.getFooter();
+ View footer = null;
+ if (footerPresentation != null) {
+ if (clickBlocker == null) { // already set for header
+ clickBlocker = newClickBlocker();
+ }
+ footer = footerPresentation.apply(context, null, clickBlocker);
+ totalItems++;
+ }
+ if (sVerbose) {
+ Slog.v(TAG, "Number datasets: " + datasetCount + " Total items: " + totalItems);
+ }
+
+ final ArrayList<ViewItem> items = new ArrayList<>(totalItems);
+ if (header != null) {
+ if (sVerbose) Slog.v(TAG, "adding header");
+ items.add(new ViewItem(null, null, null, header));
+ }
for (int i = 0; i < datasetCount; i++) {
final Dataset dataset = response.getDatasets().get(i);
final int index = dataset.getFieldIds().indexOf(focusedViewId);
@@ -176,9 +209,7 @@ final class FillUi {
String valueText = null;
if (filter == null) {
final AutofillValue value = dataset.getFieldValues().get(index);
- // If the dataset needs auth - don't add its text to allow guessing
- // its content based on how filtering behaves.
- if (value != null && value.isText() && dataset.getAuthentication() == null) {
+ if (value != null && value.isText()) {
valueText = value.getTextValue().toString().toLowerCase();
}
}
@@ -186,6 +217,10 @@ final class FillUi {
items.add(new ViewItem(dataset, filter, valueText, view));
}
}
+ if (footer != null) {
+ if (sVerbose) Slog.v(TAG, "adding footer");
+ items.add(new ViewItem(null, null, null, footer));
+ }
mAdapter = new ItemsAdapter(items);
@@ -194,7 +229,12 @@ final class FillUi {
mListView.setVisibility(View.VISIBLE);
mListView.setOnItemClickListener((adapter, view, position, id) -> {
final ViewItem vi = mAdapter.getItem(position);
- mCallback.onDatasetPicked(vi.getDataset());
+ if (vi.dataset == null) {
+ // Clicked on header or footer; ignore.
+ if (sDebug) Slog.d(TAG, "Ignoring click on item " + position + ": " + view);
+ return;
+ }
+ mCallback.onDatasetPicked(vi.dataset);
});
if (filterText == null) {
@@ -208,6 +248,20 @@ final class FillUi {
}
}
+ /**
+ * Creates a remoteview interceptor used to block clicks.
+ */
+ private RemoteViews.OnClickHandler newClickBlocker() {
+ return new RemoteViews.OnClickHandler() {
+ @Override
+ public boolean onClickHandler(View view, PendingIntent pendingIntent,
+ Intent fillInIntent) {
+ if (sVerbose) Slog.v(TAG, "Ignoring click on " + view);
+ return true;
+ }
+ };
+ }
+
private void applyNewFilterText() {
final int oldCount = mAdapter.getCount();
mAdapter.getFilter().filter(mFilterText, (count) -> {
@@ -300,7 +354,7 @@ final class FillUi {
MeasureSpec.AT_MOST);
final int itemCount = mAdapter.getCount();
for (int i = 0; i < itemCount; i++) {
- View view = mAdapter.getItem(i).getView();
+ View view = mAdapter.getItem(i).view;
view.measure(widthMeasureSpec, heightMeasureSpec);
final int clampedMeasuredWidth = Math.min(view.getMeasuredWidth(), maxSize.x);
final int newContentWidth = Math.max(mContentWidth, clampedMeasuredWidth);
@@ -338,33 +392,21 @@ final class FillUi {
outPoint.y = (int) typedValue.getFraction(outPoint.y, outPoint.y);
}
+ /**
+ * An item for the list view - either a (clickable) dataset or a (read-only) header / footer.
+ */
private static class ViewItem {
- private final String mValue;
- private final Dataset mDataset;
- private final View mView;
- private final Pattern mFilter;
-
- ViewItem(Dataset dataset, Pattern filter, String value, View view) {
- mDataset = dataset;
- mValue = value;
- mView = view;
- mFilter = filter;
- }
-
- public Pattern getFilter() {
- return mFilter;
- }
-
- public View getView() {
- return mView;
- }
-
- public Dataset getDataset() {
- return mDataset;
- }
-
- public String getValue() {
- return mValue;
+ public final @Nullable String value;
+ public final @Nullable Dataset dataset;
+ public final @NonNull View view;
+ public final @Nullable Pattern filter;
+
+ ViewItem(@Nullable Dataset dataset, @Nullable Pattern filter, @Nullable String value,
+ @NonNull View view) {
+ this.dataset = dataset;
+ this.value = value;
+ this.view = view;
+ this.filter = filter;
}
}
@@ -527,15 +569,13 @@ final class FillUi {
final int itemCount = mAllItems.size();
for (int i = 0; i < itemCount; i++) {
final ViewItem item = mAllItems.get(i);
- final String value = item.getValue();
- final Pattern filter = item.getFilter();
final boolean matches;
- if (filter != null) {
- matches = filter.matcher(constraintLowerCase).matches();
+ if (item.filter != null) {
+ matches = item.filter.matcher(constraintLowerCase).matches();
} else {
- matches = (value == null)
- ? (item.mDataset.getAuthentication() == null)
- : value.toLowerCase().startsWith(constraintLowerCase);
+ matches = (item.value == null)
+ ? (item.dataset.getAuthentication() == null)
+ : item.value.toLowerCase().startsWith(constraintLowerCase);
}
if (matches) {
filteredItems.add(item);
@@ -582,7 +622,7 @@ final class FillUi {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
- return getItem(position).getView();
+ return getItem(position).view;
}
}
diff --git a/com/android/server/backup/BackupManagerConstants.java b/com/android/server/backup/BackupManagerConstants.java
index 245241cb..537592e3 100644
--- a/com/android/server/backup/BackupManagerConstants.java
+++ b/com/android/server/backup/BackupManagerConstants.java
@@ -24,6 +24,7 @@ import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.KeyValueListParser;
import android.util.Slog;
@@ -51,6 +52,8 @@ class BackupManagerConstants extends ContentObserver {
"full_backup_require_charging";
private static final String FULL_BACKUP_REQUIRED_NETWORK_TYPE =
"full_backup_required_network_type";
+ private static final String BACKUP_FINISHED_NOTIFICATION_RECEIVERS =
+ "backup_finished_notification_receivers";
// Hard coded default values.
private static final long DEFAULT_KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS =
@@ -63,6 +66,7 @@ class BackupManagerConstants extends ContentObserver {
24 * AlarmManager.INTERVAL_HOUR;
private static final boolean DEFAULT_FULL_BACKUP_REQUIRE_CHARGING = true;
private static final int DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE = 2;
+ private static final String DEFAULT_BACKUP_FINISHED_NOTIFICATION_RECEIVERS = "";
// Backup manager constants.
private long mKeyValueBackupIntervalMilliseconds;
@@ -72,6 +76,7 @@ class BackupManagerConstants extends ContentObserver {
private long mFullBackupIntervalMilliseconds;
private boolean mFullBackupRequireCharging;
private int mFullBackupRequiredNetworkType;
+ private String[] mBackupFinishedNotificationReceivers;
private ContentResolver mResolver;
private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -116,6 +121,14 @@ class BackupManagerConstants extends ContentObserver {
DEFAULT_FULL_BACKUP_REQUIRE_CHARGING);
mFullBackupRequiredNetworkType = mParser.getInt(FULL_BACKUP_REQUIRED_NETWORK_TYPE,
DEFAULT_FULL_BACKUP_REQUIRED_NETWORK_TYPE);
+ final String backupFinishedNotificationReceivers = mParser.getString(
+ BACKUP_FINISHED_NOTIFICATION_RECEIVERS,
+ DEFAULT_BACKUP_FINISHED_NOTIFICATION_RECEIVERS);
+ if (backupFinishedNotificationReceivers.isEmpty()) {
+ mBackupFinishedNotificationReceivers = new String[] {};
+ } else {
+ mBackupFinishedNotificationReceivers = backupFinishedNotificationReceivers.split(":");
+ }
}
// The following are access methods for the individual parameters.
@@ -167,7 +180,6 @@ class BackupManagerConstants extends ContentObserver {
Slog.v(TAG, "getFullBackupRequireCharging(...) returns " + mFullBackupRequireCharging);
}
return mFullBackupRequireCharging;
-
}
public synchronized int getFullBackupRequiredNetworkType() {
@@ -177,4 +189,12 @@ class BackupManagerConstants extends ContentObserver {
}
return mFullBackupRequiredNetworkType;
}
+
+ public synchronized String[] getBackupFinishedNotificationReceivers() {
+ if (RefactoredBackupManagerService.DEBUG_SCHEDULING) {
+ Slog.v(TAG, "getBackupFinishedNotificationReceivers(...) returns "
+ + TextUtils.join(", ", mBackupFinishedNotificationReceivers));
+ }
+ return mBackupFinishedNotificationReceivers;
+ }
}
diff --git a/com/android/server/backup/BackupManagerConstantsTest.java b/com/android/server/backup/BackupManagerConstantsTest.java
new file mode 100644
index 00000000..c20c3767
--- /dev/null
+++ b/com/android/server/backup/BackupManagerConstantsTest.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 com.android.server.backup;
+
+import android.app.AlarmManager;
+import android.content.Context;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+
+import com.android.server.backup.testing.ShadowBackupTransportStub;
+import com.android.server.backup.testing.ShadowContextImplForBackup;
+import com.android.server.backup.testing.ShadowPackageManagerForBackup;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderClasses;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(
+ manifest = Config.NONE,
+ sdk = 26,
+ shadows = {
+ ShadowContextImplForBackup.class,
+ ShadowBackupTransportStub.class,
+ ShadowPackageManagerForBackup.class
+ }
+)
+@SystemLoaderClasses({TransportManager.class})
+@Presubmit
+public class BackupManagerConstantsTest {
+ private static final String PACKAGE_NAME = "some.package.name";
+ private static final String ANOTHER_PACKAGE_NAME = "another.package.name";
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Test
+ public void testDefaultValues() throws Exception {
+ final Context context = RuntimeEnvironment.application.getApplicationContext();
+ final Handler handler = new Handler();
+
+ Settings.Secure.putString(context.getContentResolver(),
+ Settings.Secure.BACKUP_MANAGER_CONSTANTS, null);
+
+ final BackupManagerConstants constants =
+ new BackupManagerConstants(handler, context.getContentResolver());
+ constants.start();
+
+ assertThat(constants.getKeyValueBackupIntervalMilliseconds())
+ .isEqualTo(4 * AlarmManager.INTERVAL_HOUR);
+ assertThat(constants.getKeyValueBackupFuzzMilliseconds()).isEqualTo(10 * 60 * 1000);
+ assertThat(constants.getKeyValueBackupRequireCharging()).isEqualTo(true);
+ assertThat(constants.getKeyValueBackupRequiredNetworkType()).isEqualTo(1);
+
+ assertThat(constants.getFullBackupIntervalMilliseconds())
+ .isEqualTo(24 * AlarmManager.INTERVAL_HOUR);
+ assertThat(constants.getFullBackupRequireCharging()).isEqualTo(true);
+ assertThat(constants.getFullBackupRequiredNetworkType()).isEqualTo(2);
+ assertThat(constants.getBackupFinishedNotificationReceivers()).isEqualTo(new String[0]);
+ }
+
+ @Test
+ public void testParseNotificationReceivers() throws Exception {
+ final Context context = RuntimeEnvironment.application.getApplicationContext();
+ final Handler handler = new Handler();
+
+ final String recievers_setting = "backup_finished_notification_receivers=" +
+ PACKAGE_NAME + ':' + ANOTHER_PACKAGE_NAME;
+ Settings.Secure.putString(context.getContentResolver(),
+ Settings.Secure.BACKUP_MANAGER_CONSTANTS, recievers_setting);
+
+ final BackupManagerConstants constants =
+ new BackupManagerConstants(handler, context.getContentResolver());
+ constants.start();
+
+ assertThat(constants.getBackupFinishedNotificationReceivers()).isEqualTo(new String[] {
+ PACKAGE_NAME,
+ ANOTHER_PACKAGE_NAME});
+ }
+}
diff --git a/com/android/server/backup/BackupManagerService.java b/com/android/server/backup/BackupManagerService.java
deleted file mode 100644
index e92a5647..00000000
--- a/com/android/server/backup/BackupManagerService.java
+++ /dev/null
@@ -1,11482 +0,0 @@
-/*
- * 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 com.android.server.backup;
-
-import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_NAME;
-import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION;
-import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_OLD_VERSION;
-import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_POLICY_ALLOW_APKS;
-import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_MANIFEST_PACKAGE_NAME;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_SYSTEM_APP_NO_AGENT;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_APK_NOT_INSTALLED;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_MISSING_SIGNATURE;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_RESTORE_ANY_VERSION;
-import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSIONS_MATCH;
-import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_BACKUP_IN_FOREGROUND;
-
-import android.app.ActivityManager;
-import android.app.AlarmManager;
-import android.app.AppGlobals;
-import android.app.ApplicationThreadConstants;
-import android.app.IActivityManager;
-import android.app.IBackupAgent;
-import android.app.PackageInstallObserver;
-import android.app.PendingIntent;
-import android.app.backup.BackupAgent;
-import android.app.backup.BackupDataInput;
-import android.app.backup.BackupDataOutput;
-import android.app.backup.BackupManager;
-import android.app.backup.BackupManagerMonitor;
-import android.app.backup.BackupProgress;
-import android.app.backup.BackupTransport;
-import android.app.backup.FullBackup;
-import android.app.backup.FullBackupDataOutput;
-import android.app.backup.IBackupManager;
-import android.app.backup.IBackupManagerMonitor;
-import android.app.backup.IBackupObserver;
-import android.app.backup.IFullBackupRestoreObserver;
-import android.app.backup.IRestoreObserver;
-import android.app.backup.IRestoreSession;
-import android.app.backup.ISelectBackupTransportCallback;
-import android.app.backup.RestoreDescription;
-import android.app.backup.RestoreSet;
-import android.app.backup.SelectBackupTransportCallback;
-import android.content.ActivityNotFoundException;
-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.ServiceConnection;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageDataObserver;
-import android.content.pm.IPackageDeleteObserver;
-import android.content.pm.IPackageManager;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.Signature;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.PowerManager.ServiceType;
-import android.os.PowerSaveState;
-import android.os.Binder;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.Environment.UserEnvironment;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.ParcelFileDescriptor;
-import android.os.PowerManager;
-import android.os.Process;
-import android.os.RemoteException;
-import android.os.SELinux;
-import android.os.ServiceManager;
-import android.os.SystemClock;
-import android.os.Trace;
-import android.os.UserHandle;
-import android.os.WorkSource;
-import android.os.storage.IStorageManager;
-import android.os.storage.StorageManager;
-import android.provider.Settings;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.AtomicFile;
-import android.util.EventLog;
-import android.util.Log;
-import android.util.Pair;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.util.StringBuilderPrinter;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.backup.IBackupTransport;
-import com.android.internal.backup.IObbBackupService;
-import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.DumpUtils;
-import com.android.server.AppWidgetBackupBridge;
-import com.android.server.EventLogTags;
-import com.android.server.SystemConfig;
-import com.android.server.SystemService;
-import com.android.server.backup.PackageManagerBackupAgent.Metadata;
-
-import libcore.io.IoUtils;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.EOFException;
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintWriter;
-import java.io.RandomAccessFile;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
-import java.text.SimpleDateFormat;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.Objects;
-import java.util.Queue;
-import java.util.Random;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.zip.Deflater;
-import java.util.zip.DeflaterOutputStream;
-import java.util.zip.InflaterInputStream;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.CipherInputStream;
-import javax.crypto.CipherOutputStream;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.PBEKeySpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * @Deprecated Use RefactoredBackupManagerService instead. This class is only
- * kept for fallback and archeology reasons and will be removed soon.
- */
-public class BackupManagerService implements BackupManagerServiceInterface {
-
- private static final String TAG = "BackupManagerService";
- static final boolean DEBUG = true;
- static final boolean MORE_DEBUG = false;
- static final boolean DEBUG_SCHEDULING = MORE_DEBUG || true;
-
- // File containing backup-enabled state. Contains a single byte;
- // nonzero == enabled. File missing or contains a zero byte == disabled.
- static final String BACKUP_ENABLE_FILE = "backup_enabled";
-
- // System-private key used for backing up an app's widget state. Must
- // begin with U+FFxx by convention (we reserve all keys starting
- // with U+FF00 or higher for system use).
- static final String KEY_WIDGET_STATE = "\uffed\uffedwidget";
-
- // Historical and current algorithm names
- static final String PBKDF_CURRENT = "PBKDF2WithHmacSHA1";
- static final String PBKDF_FALLBACK = "PBKDF2WithHmacSHA1And8bit";
-
- // Name and current contents version of the full-backup manifest file
- //
- // Manifest version history:
- //
- // 1 : initial release
- static final String BACKUP_MANIFEST_FILENAME = "_manifest";
- static final int BACKUP_MANIFEST_VERSION = 1;
-
- // External archive format version history:
- //
- // 1 : initial release
- // 2 : no format change per se; version bump to facilitate PBKDF2 version skew detection
- // 3 : introduced "_meta" metadata file; no other format change per se
- // 4 : added support for new device-encrypted storage locations
- // 5 : added support for key-value packages
- static final int BACKUP_FILE_VERSION = 5;
- static final String BACKUP_FILE_HEADER_MAGIC = "ANDROID BACKUP\n";
- static final int BACKUP_PW_FILE_VERSION = 2;
- static final String BACKUP_METADATA_FILENAME = "_meta";
- static final int BACKUP_METADATA_VERSION = 1;
- static final int BACKUP_WIDGET_METADATA_TOKEN = 0x01FFED01;
-
- static final int TAR_HEADER_LONG_RADIX = 8;
- static final int TAR_HEADER_OFFSET_FILESIZE = 124;
- static final int TAR_HEADER_LENGTH_FILESIZE = 12;
- static final int TAR_HEADER_OFFSET_MODTIME = 136;
- static final int TAR_HEADER_LENGTH_MODTIME = 12;
- static final int TAR_HEADER_OFFSET_MODE = 100;
- static final int TAR_HEADER_LENGTH_MODE = 8;
- static final int TAR_HEADER_OFFSET_PATH_PREFIX = 345;
- static final int TAR_HEADER_LENGTH_PATH_PREFIX = 155;
- static final int TAR_HEADER_OFFSET_PATH = 0;
- static final int TAR_HEADER_LENGTH_PATH = 100;
- static final int TAR_HEADER_OFFSET_TYPE_CHAR = 156;
-
- static final boolean COMPRESS_FULL_BACKUPS = true; // should be true in production
-
- static final String SETTINGS_PACKAGE = "com.android.providers.settings";
- static final String SHARED_BACKUP_AGENT_PACKAGE = "com.android.sharedstoragebackup";
- static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST";
-
- // Retry interval for clear/init when the transport is unavailable
- private static final long TRANSPORT_RETRY_INTERVAL = 1 * AlarmManager.INTERVAL_HOUR;
-
- private static final String RUN_BACKUP_ACTION = "android.app.backup.intent.RUN";
- private static final String RUN_INITIALIZE_ACTION = "android.app.backup.intent.INIT";
- private static final int MSG_RUN_BACKUP = 1;
- private static final int MSG_RUN_ADB_BACKUP = 2;
- private static final int MSG_RUN_RESTORE = 3;
- private static final int MSG_RUN_CLEAR = 4;
- private static final int MSG_RUN_GET_RESTORE_SETS = 6;
- private static final int MSG_RESTORE_SESSION_TIMEOUT = 8;
- private static final int MSG_FULL_CONFIRMATION_TIMEOUT = 9;
- private static final int MSG_RUN_ADB_RESTORE = 10;
- private static final int MSG_RETRY_INIT = 11;
- private static final int MSG_RETRY_CLEAR = 12;
- private static final int MSG_WIDGET_BROADCAST = 13;
- private static final int MSG_RUN_FULL_TRANSPORT_BACKUP = 14;
- private static final int MSG_REQUEST_BACKUP = 15;
- private static final int MSG_SCHEDULE_BACKUP_PACKAGE = 16;
- private static final int MSG_BACKUP_OPERATION_TIMEOUT = 17;
- private static final int MSG_RESTORE_OPERATION_TIMEOUT = 18;
-
- // backup task state machine tick
- static final int MSG_BACKUP_RESTORE_STEP = 20;
- static final int MSG_OP_COMPLETE = 21;
-
- // Timeout interval for deciding that a bind or clear-data has taken too long
- static final long TIMEOUT_INTERVAL = 10 * 1000;
-
- // Timeout intervals for agent backup & restore operations
- static final long TIMEOUT_BACKUP_INTERVAL = 30 * 1000;
- static final long TIMEOUT_FULL_BACKUP_INTERVAL = 5 * 60 * 1000;
- static final long TIMEOUT_SHARED_BACKUP_INTERVAL = 30 * 60 * 1000;
- static final long TIMEOUT_RESTORE_INTERVAL = 60 * 1000;
- static final long TIMEOUT_RESTORE_FINISHED_INTERVAL = 30 * 1000;
-
- // User confirmation timeout for a full backup/restore operation. It's this long in
- // order to give them time to enter the backup password.
- static final long TIMEOUT_FULL_CONFIRMATION = 60 * 1000;
-
- // If an app is busy when we want to do a full-data backup, how long to defer the retry.
- // This is fuzzed, so there are two parameters; backoff_min + Rand[0, backoff_fuzz)
- static final long BUSY_BACKOFF_MIN_MILLIS = 1000 * 60 * 60; // one hour
- static final int BUSY_BACKOFF_FUZZ = 1000 * 60 * 60 * 2; // two hours
-
- BackupManagerConstants mConstants;
- Context mContext;
- private PackageManager mPackageManager;
- IPackageManager mPackageManagerBinder;
- private IActivityManager mActivityManager;
- private PowerManager mPowerManager;
- private AlarmManager mAlarmManager;
- private IStorageManager mStorageManager;
-
- IBackupManager mBackupManagerBinder;
-
- private final TransportManager mTransportManager;
-
- boolean mEnabled; // access to this is synchronized on 'this'
- boolean mProvisioned;
- boolean mAutoRestore;
- PowerManager.WakeLock mWakelock;
- BackupHandler mBackupHandler;
- PendingIntent mRunBackupIntent, mRunInitIntent;
- BroadcastReceiver mRunBackupReceiver, mRunInitReceiver;
- // map UIDs to the set of participating packages under that UID
- final SparseArray<HashSet<String>> mBackupParticipants
- = new SparseArray<HashSet<String>>();
- // set of backup services that have pending changes
- class BackupRequest {
- public String packageName;
-
- BackupRequest(String pkgName) {
- packageName = pkgName;
- }
-
- public String toString() {
- return "BackupRequest{pkg=" + packageName + "}";
- }
- }
- // Backups that we haven't started yet. Keys are package names.
- HashMap<String,BackupRequest> mPendingBackups
- = new HashMap<String,BackupRequest>();
-
- // Pseudoname that we use for the Package Manager metadata "package"
- static final String PACKAGE_MANAGER_SENTINEL = "@pm@";
-
- // locking around the pending-backup management
- final Object mQueueLock = new Object();
-
- // The thread performing the sequence of queued backups binds to each app's agent
- // in succession. Bind notifications are asynchronously delivered through the
- // Activity Manager; use this lock object to signal when a requested binding has
- // completed.
- final Object mAgentConnectLock = new Object();
- IBackupAgent mConnectedAgent;
- volatile boolean mBackupRunning;
- volatile boolean mConnecting;
- volatile long mLastBackupPass;
-
- // For debugging, we maintain a progress trace of operations during backup
- static final boolean DEBUG_BACKUP_TRACE = true;
- final List<String> mBackupTrace = new ArrayList<String>();
-
- // A similar synchronization mechanism around clearing apps' data for restore
- final Object mClearDataLock = new Object();
- volatile boolean mClearingData;
-
- @GuardedBy("mPendingRestores")
- private boolean mIsRestoreInProgress;
- @GuardedBy("mPendingRestores")
- private final Queue<PerformUnifiedRestoreTask> mPendingRestores = new ArrayDeque<>();
-
- ActiveRestoreSession mActiveRestoreSession;
-
- // Watch the device provisioning operation during setup
- ContentObserver mProvisionedObserver;
-
- // The published binder is actually to a singleton trampoline object that calls
- // through to the proper code. This indirection lets us turn down the heavy
- // implementation object on the fly without disturbing binders that have been
- // cached elsewhere in the system.
- static Trampoline sInstance;
- static Trampoline getInstance() {
- // Always constructed during system bringup, so no need to lazy-init
- return sInstance;
- }
-
- public static final class Lifecycle extends SystemService {
-
- public Lifecycle(Context context) {
- super(context);
- sInstance = new Trampoline(context);
- }
-
- @Override
- public void onStart() {
- publishBinderService(Context.BACKUP_SERVICE, sInstance);
- }
-
- @Override
- public void onUnlockUser(int userId) {
- if (userId == UserHandle.USER_SYSTEM) {
- sInstance.unlockSystemUser();
- }
- }
- }
-
- // Called through the trampoline from onUnlockUser(), then we buck the work
- // off to the background thread to keep the unlock time down.
- public void unlockSystemUser() {
- // Migrate legacy setting
- Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backup migrate");
- if (!backupSettingMigrated(UserHandle.USER_SYSTEM)) {
- if (DEBUG) {
- Slog.i(TAG, "Backup enable apparently not migrated");
- }
- final ContentResolver r = sInstance.mContext.getContentResolver();
- final int enableState = Settings.Secure.getIntForUser(r,
- Settings.Secure.BACKUP_ENABLED, -1, UserHandle.USER_SYSTEM);
- if (enableState >= 0) {
- if (DEBUG) {
- Slog.i(TAG, "Migrating enable state " + (enableState != 0));
- }
- writeBackupEnableState(enableState != 0, UserHandle.USER_SYSTEM);
- Settings.Secure.putStringForUser(r,
- Settings.Secure.BACKUP_ENABLED, null, UserHandle.USER_SYSTEM);
- } else {
- if (DEBUG) {
- Slog.i(TAG, "Backup not yet configured; retaining null enable state");
- }
- }
- }
- Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
-
- Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backup enable");
- try {
- sInstance.setBackupEnabled(readBackupEnableState(UserHandle.USER_SYSTEM));
- } catch (RemoteException e) {
- // can't happen; it's a local object
- }
- Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
- }
-
- class ProvisionedObserver extends ContentObserver {
- public ProvisionedObserver(Handler handler) {
- super(handler);
- }
-
- public void onChange(boolean selfChange) {
- final boolean wasProvisioned = mProvisioned;
- final boolean isProvisioned = deviceIsProvisioned();
- // latch: never unprovision
- mProvisioned = wasProvisioned || isProvisioned;
- if (MORE_DEBUG) {
- Slog.d(TAG, "Provisioning change: was=" + wasProvisioned
- + " is=" + isProvisioned + " now=" + mProvisioned);
- }
-
- synchronized (mQueueLock) {
- if (mProvisioned && !wasProvisioned && mEnabled) {
- // we're now good to go, so start the backup alarms
- if (MORE_DEBUG) Slog.d(TAG, "Now provisioned, so starting backups");
- KeyValueBackupJob.schedule(mContext, mConstants);
- scheduleNextFullBackupJob(0);
- }
- }
- }
- }
-
- class RestoreGetSetsParams {
- public IBackupTransport transport;
- public ActiveRestoreSession session;
- public IRestoreObserver observer;
- public IBackupManagerMonitor monitor;
-
- RestoreGetSetsParams(IBackupTransport _transport, ActiveRestoreSession _session,
- IRestoreObserver _observer, IBackupManagerMonitor _monitor) {
- transport = _transport;
- session = _session;
- observer = _observer;
- monitor = _monitor;
- }
- }
-
- class RestoreParams {
- public IBackupTransport transport;
- public String dirName;
- public IRestoreObserver observer;
- public IBackupManagerMonitor monitor;
- public long token;
- public PackageInfo pkgInfo;
- public int pmToken; // in post-install restore, the PM's token for this transaction
- public boolean isSystemRestore;
- public String[] filterSet;
-
- /**
- * Restore a single package; no kill after restore
- */
- RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs,
- IBackupManagerMonitor _monitor, long _token, PackageInfo _pkg) {
- transport = _transport;
- dirName = _dirName;
- observer = _obs;
- monitor = _monitor;
- token = _token;
- pkgInfo = _pkg;
- pmToken = 0;
- isSystemRestore = false;
- filterSet = null;
- }
-
- /**
- * Restore at install: PM token needed, kill after restore
- */
- RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs,
- IBackupManagerMonitor _monitor, long _token, String _pkgName, int _pmToken) {
- transport = _transport;
- dirName = _dirName;
- observer = _obs;
- monitor = _monitor;
- token = _token;
- pkgInfo = null;
- pmToken = _pmToken;
- isSystemRestore = false;
- filterSet = new String[] { _pkgName };
- }
-
- /**
- * Restore everything possible. This is the form that Setup Wizard or similar
- * restore UXes use.
- */
- RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs,
- IBackupManagerMonitor _monitor, long _token) {
- transport = _transport;
- dirName = _dirName;
- observer = _obs;
- monitor = _monitor;
- token = _token;
- pkgInfo = null;
- pmToken = 0;
- isSystemRestore = true;
- filterSet = null;
- }
-
- /**
- * Restore some set of packages. Leave this one up to the caller to specify
- * whether it's to be considered a system-level restore.
- */
- RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs,
- IBackupManagerMonitor _monitor, long _token,
- String[] _filterSet, boolean _isSystemRestore) {
- transport = _transport;
- dirName = _dirName;
- observer = _obs;
- monitor = _monitor;
- token = _token;
- pkgInfo = null;
- pmToken = 0;
- isSystemRestore = _isSystemRestore;
- filterSet = _filterSet;
- }
- }
-
- class ClearParams {
- public IBackupTransport transport;
- public PackageInfo packageInfo;
-
- ClearParams(IBackupTransport _transport, PackageInfo _info) {
- transport = _transport;
- packageInfo = _info;
- }
- }
-
- class ClearRetryParams {
- public String transportName;
- public String packageName;
-
- ClearRetryParams(String transport, String pkg) {
- transportName = transport;
- packageName = pkg;
- }
- }
-
- // Parameters used by adbBackup() and adbRestore()
- class AdbParams {
- public ParcelFileDescriptor fd;
- public final AtomicBoolean latch;
- public IFullBackupRestoreObserver observer;
- public String curPassword; // filled in by the confirmation step
- public String encryptPassword;
-
- AdbParams() {
- latch = new AtomicBoolean(false);
- }
- }
-
- class AdbBackupParams extends AdbParams {
- public boolean includeApks;
- public boolean includeObbs;
- public boolean includeShared;
- public boolean doWidgets;
- public boolean allApps;
- public boolean includeSystem;
- public boolean doCompress;
- public boolean includeKeyValue;
- public String[] packages;
-
- AdbBackupParams(ParcelFileDescriptor output, boolean saveApks, boolean saveObbs,
- boolean saveShared, boolean alsoWidgets, boolean doAllApps, boolean doSystem,
- boolean compress, boolean doKeyValue, String[] pkgList) {
- fd = output;
- includeApks = saveApks;
- includeObbs = saveObbs;
- includeShared = saveShared;
- doWidgets = alsoWidgets;
- allApps = doAllApps;
- includeSystem = doSystem;
- doCompress = compress;
- includeKeyValue = doKeyValue;
- packages = pkgList;
- }
- }
-
- class AdbRestoreParams extends AdbParams {
- AdbRestoreParams(ParcelFileDescriptor input) {
- fd = input;
- }
- }
-
- class BackupParams {
- public IBackupTransport transport;
- public String dirName;
- public ArrayList<String> kvPackages;
- public ArrayList<String> fullPackages;
- public IBackupObserver observer;
- public IBackupManagerMonitor monitor;
- public boolean userInitiated;
- public boolean nonIncrementalBackup;
-
- BackupParams(IBackupTransport transport, String dirName, ArrayList<String> kvPackages,
- ArrayList<String> fullPackages, IBackupObserver observer,
- IBackupManagerMonitor monitor,boolean userInitiated, boolean nonIncrementalBackup) {
- this.transport = transport;
- this.dirName = dirName;
- this.kvPackages = kvPackages;
- this.fullPackages = fullPackages;
- this.observer = observer;
- this.monitor = monitor;
- this.userInitiated = userInitiated;
- this.nonIncrementalBackup = nonIncrementalBackup;
- }
- }
-
- // Bookkeeping of in-flight operations for timeout etc. purposes. The operation
- // token is the index of the entry in the pending-operations list.
- static final int OP_PENDING = 0;
- static final int OP_ACKNOWLEDGED = 1;
- static final int OP_TIMEOUT = -1;
-
- // Waiting for backup agent to respond during backup operation.
- static final int OP_TYPE_BACKUP_WAIT = 0;
-
- // Waiting for backup agent to respond during restore operation.
- static final int OP_TYPE_RESTORE_WAIT = 1;
-
- // An entire backup operation spanning multiple packages.
- private static final int OP_TYPE_BACKUP = 2;
-
- class Operation {
- int state;
- final BackupRestoreTask callback;
- final int type;
-
- Operation(int initialState, BackupRestoreTask callbackObj, int type) {
- state = initialState;
- callback = callbackObj;
- this.type = type;
- }
- }
-
- /**
- * mCurrentOperations contains the list of currently active operations.
- *
- * If type of operation is OP_TYPE_WAIT, it are waiting for an ack or timeout.
- * An operation wraps a BackupRestoreTask within it.
- * It's the responsibility of this task to remove the operation from this array.
- *
- * A BackupRestore task gets notified of ack/timeout for the operation via
- * BackupRestoreTask#handleCancel, BackupRestoreTask#operationComplete and notifyAll called
- * on the mCurrentOpLock. {@link BackupManagerService#waitUntilOperationComplete(int)} is
- * used in various places to 'wait' for notifyAll and detect change of pending state of an
- * operation. So typically, an operation will be removed from this array by:
- * - BackupRestoreTask#handleCancel and
- * - BackupRestoreTask#operationComplete OR waitUntilOperationComplete. Do not remove at both
- * these places because waitUntilOperationComplete relies on the operation being present to
- * determine its completion status.
- *
- * If type of operation is OP_BACKUP, it is a task running backups. It provides a handle to
- * cancel backup tasks.
- */
- @GuardedBy("mCurrentOpLock")
- final SparseArray<Operation> mCurrentOperations = new SparseArray<Operation>();
- final Object mCurrentOpLock = new Object();
- final Random mTokenGenerator = new Random();
- final AtomicInteger mNextToken = new AtomicInteger();
-
- final SparseArray<AdbParams> mAdbBackupRestoreConfirmations = new SparseArray<AdbParams>();
-
- // Where we keep our journal files and other bookkeeping
- File mBaseStateDir;
- File mDataDir;
- File mJournalDir;
- File mJournal;
-
- // Backup password, if any, and the file where it's saved. What is stored is not the
- // password text itself; it's the result of a PBKDF2 hash with a randomly chosen (but
- // persisted) salt. Validation is performed by running the challenge text through the
- // same PBKDF2 cycle with the persisted salt; if the resulting derived key string matches
- // the saved hash string, then the challenge text matches the originally supplied
- // password text.
- private final SecureRandom mRng = new SecureRandom();
- private String mPasswordHash;
- private File mPasswordHashFile;
- private int mPasswordVersion;
- private File mPasswordVersionFile;
- private byte[] mPasswordSalt;
-
- // Configuration of PBKDF2 that we use for generating pw hashes and intermediate keys
- static final int PBKDF2_HASH_ROUNDS = 10000;
- static final int PBKDF2_KEY_SIZE = 256; // bits
- static final int PBKDF2_SALT_SIZE = 512; // bits
- static final String ENCRYPTION_ALGORITHM_NAME = "AES-256";
-
- // Keep a log of all the apps we've ever backed up, and what the
- // dataset tokens are for both the current backup dataset and
- // the ancestral dataset.
- private File mEverStored;
- HashSet<String> mEverStoredApps = new HashSet<String>();
-
- static final int CURRENT_ANCESTRAL_RECORD_VERSION = 1; // increment when the schema changes
- File mTokenFile;
- Set<String> mAncestralPackages = null;
- long mAncestralToken = 0;
- long mCurrentToken = 0;
-
- // Persistently track the need to do a full init
- static final String INIT_SENTINEL_FILE_NAME = "_need_init_";
- ArraySet<String> mPendingInits = new ArraySet<String>(); // transport names
-
- // Round-robin queue for scheduling full backup passes
- static final int SCHEDULE_FILE_VERSION = 1; // current version of the schedule file
- class FullBackupEntry implements Comparable<FullBackupEntry> {
- String packageName;
- long lastBackup;
-
- FullBackupEntry(String pkg, long when) {
- packageName = pkg;
- lastBackup = when;
- }
-
- @Override
- public int compareTo(FullBackupEntry other) {
- if (lastBackup < other.lastBackup) return -1;
- else if (lastBackup > other.lastBackup) return 1;
- else return 0;
- }
- }
-
- File mFullBackupScheduleFile;
- // If we're running a schedule-driven full backup, this is the task instance doing it
-
- @GuardedBy("mQueueLock")
- PerformFullTransportBackupTask mRunningFullBackupTask;
-
- @GuardedBy("mQueueLock")
- ArrayList<FullBackupEntry> mFullBackupQueue;
-
- // Utility: build a new random integer token. The low bits are the ordinal of the
- // operation for near-time uniqueness, and the upper bits are random for app-
- // side unpredictability.
- @Override
- public int generateRandomIntegerToken() {
- int token = mTokenGenerator.nextInt();
- if (token < 0) token = -token;
- token &= ~0xFF;
- token |= (mNextToken.incrementAndGet() & 0xFF);
- return token;
- }
-
- // High level policy: apps are generally ineligible for backup if certain conditions apply
- public static boolean appIsEligibleForBackup(ApplicationInfo app, PackageManager pm) {
- // 1. their manifest states android:allowBackup="false"
- if ((app.flags&ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) {
- return false;
- }
-
- // 2. they run as a system-level uid but do not supply their own backup agent
- if ((app.uid < Process.FIRST_APPLICATION_UID) && (app.backupAgentName == null)) {
- return false;
- }
-
- // 3. it is the special shared-storage backup package used for 'adb backup'
- if (app.packageName.equals(BackupManagerService.SHARED_BACKUP_AGENT_PACKAGE)) {
- return false;
- }
-
- // 4. it is an "instant" app
- if (app.isInstantApp()) {
- return false;
- }
-
- // Everything else checks out; the only remaining roadblock would be if the
- // package were disabled
- return !appIsDisabled(app, pm);
- }
-
- // Checks if the app is in a stopped state. This is not part of the general "eligible for
- // backup?" check because we *do* still need to restore data to apps in this state (e.g.
- // newly-installing ones)
- private static boolean appIsStopped(ApplicationInfo app) {
- return ((app.flags & ApplicationInfo.FLAG_STOPPED) != 0);
- }
-
- private static boolean appIsDisabled(ApplicationInfo app, PackageManager pm) {
- switch (pm.getApplicationEnabledSetting(app.packageName)) {
- case PackageManager.COMPONENT_ENABLED_STATE_DISABLED:
- case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER:
- case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED:
- return true;
-
- default:
- return false;
- }
- }
-
- /* does *not* check overall backup eligibility policy! */
- private static boolean appGetsFullBackup(PackageInfo pkg) {
- if (pkg.applicationInfo.backupAgentName != null) {
- // If it has an agent, it gets full backups only if it says so
- return (pkg.applicationInfo.flags & ApplicationInfo.FLAG_FULL_BACKUP_ONLY) != 0;
- }
-
- // No agent or fullBackupOnly="true" means we do indeed perform full-data backups for it
- return true;
- }
-
- /* adb backup: is this app only capable of doing key/value? We say otherwise if
- * the app has a backup agent and does not say fullBackupOnly,
- */
- private static boolean appIsKeyValueOnly(PackageInfo pkg) {
- return !appGetsFullBackup(pkg);
- }
-
- /*
- * Construct a backup agent instance for the metadata pseudopackage. This is a
- * process-local non-lifecycle agent instance, so we manually set up the context
- * topology for it.
- */
- PackageManagerBackupAgent makeMetadataAgent() {
- PackageManagerBackupAgent pmAgent = new PackageManagerBackupAgent(mPackageManager);
- pmAgent.attach(mContext);
- pmAgent.onCreate();
- return pmAgent;
- }
-
- /*
- * Same as above but with the explicit package-set configuration.
- */
- PackageManagerBackupAgent makeMetadataAgent(List<PackageInfo> packages) {
- PackageManagerBackupAgent pmAgent =
- new PackageManagerBackupAgent(mPackageManager, packages);
- pmAgent.attach(mContext);
- pmAgent.onCreate();
- return pmAgent;
- }
-
- // ----- Asynchronous backup/restore handler thread -----
-
- private class BackupHandler extends Handler {
- public BackupHandler(Looper looper) {
- super(looper);
- }
-
- public void handleMessage(Message msg) {
-
- switch (msg.what) {
- case MSG_RUN_BACKUP:
- {
- mLastBackupPass = System.currentTimeMillis();
-
- IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
- if (transport == null) {
- Slog.v(TAG, "Backup requested but no transport available");
- synchronized (mQueueLock) {
- mBackupRunning = false;
- }
- mWakelock.release();
- break;
- }
-
- // snapshot the pending-backup set and work on that
- ArrayList<BackupRequest> queue = new ArrayList<BackupRequest>();
- File oldJournal = mJournal;
- synchronized (mQueueLock) {
- // Do we have any work to do? Construct the work queue
- // then release the synchronization lock to actually run
- // the backup.
- if (mPendingBackups.size() > 0) {
- for (BackupRequest b: mPendingBackups.values()) {
- queue.add(b);
- }
- if (DEBUG) Slog.v(TAG, "clearing pending backups");
- mPendingBackups.clear();
-
- // Start a new backup-queue journal file too
- mJournal = null;
-
- }
- }
-
- // At this point, we have started a new journal file, and the old
- // file identity is being passed to the backup processing task.
- // When it completes successfully, that old journal file will be
- // deleted. If we crash prior to that, the old journal is parsed
- // at next boot and the journaled requests fulfilled.
- boolean staged = true;
- if (queue.size() > 0) {
- // Spin up a backup state sequence and set it running
- try {
- String dirName = transport.transportDirName();
- PerformBackupTask pbt = new PerformBackupTask(transport, dirName, queue,
- oldJournal, null, null, Collections.<String>emptyList(), false,
- false /* nonIncremental */);
- Message pbtMessage = obtainMessage(MSG_BACKUP_RESTORE_STEP, pbt);
- sendMessage(pbtMessage);
- } catch (Exception e) {
- // unable to ask the transport its dir name -- transient failure, since
- // the above check succeeded. Try again next time.
- Slog.e(TAG, "Transport became unavailable attempting backup"
- + " or error initializing backup task", e);
- staged = false;
- }
- } else {
- Slog.v(TAG, "Backup requested but nothing pending");
- staged = false;
- }
-
- if (!staged) {
- // if we didn't actually hand off the wakelock, rewind until next time
- synchronized (mQueueLock) {
- mBackupRunning = false;
- }
- mWakelock.release();
- }
- break;
- }
-
- case MSG_BACKUP_RESTORE_STEP:
- {
- try {
- BackupRestoreTask task = (BackupRestoreTask) msg.obj;
- if (MORE_DEBUG) Slog.v(TAG, "Got next step for " + task + ", executing");
- task.execute();
- } catch (ClassCastException e) {
- Slog.e(TAG, "Invalid backup task in flight, obj=" + msg.obj);
- }
- break;
- }
-
- case MSG_OP_COMPLETE:
- {
- try {
- Pair<BackupRestoreTask, Long> taskWithResult =
- (Pair<BackupRestoreTask, Long>) msg.obj;
- taskWithResult.first.operationComplete(taskWithResult.second);
- } catch (ClassCastException e) {
- Slog.e(TAG, "Invalid completion in flight, obj=" + msg.obj);
- }
- break;
- }
-
- case MSG_RUN_ADB_BACKUP:
- {
- // TODO: refactor full backup to be a looper-based state machine
- // similar to normal backup/restore.
- AdbBackupParams params = (AdbBackupParams)msg.obj;
- PerformAdbBackupTask task = new PerformAdbBackupTask(params.fd,
- params.observer, params.includeApks, params.includeObbs,
- params.includeShared, params.doWidgets, params.curPassword,
- params.encryptPassword, params.allApps, params.includeSystem,
- params.doCompress, params.includeKeyValue, params.packages, params.latch);
- (new Thread(task, "adb-backup")).start();
- break;
- }
-
- case MSG_RUN_FULL_TRANSPORT_BACKUP:
- {
- PerformFullTransportBackupTask task = (PerformFullTransportBackupTask) msg.obj;
- (new Thread(task, "transport-backup")).start();
- break;
- }
-
- case MSG_RUN_RESTORE:
- {
- RestoreParams params = (RestoreParams)msg.obj;
- Slog.d(TAG, "MSG_RUN_RESTORE observer=" + params.observer);
-
- PerformUnifiedRestoreTask task = new PerformUnifiedRestoreTask(params.transport,
- params.observer, params.monitor, params.token, params.pkgInfo,
- params.pmToken, params.isSystemRestore, params.filterSet);
-
- synchronized (mPendingRestores) {
- if (mIsRestoreInProgress) {
- if (DEBUG) {
- Slog.d(TAG, "Restore in progress, queueing.");
- }
- mPendingRestores.add(task);
- // This task will be picked up and executed when the the currently running
- // restore task finishes.
- } else {
- if (DEBUG) {
- Slog.d(TAG, "Starting restore.");
- }
- mIsRestoreInProgress = true;
- Message restoreMsg = obtainMessage(MSG_BACKUP_RESTORE_STEP, task);
- sendMessage(restoreMsg);
- }
- }
- break;
- }
-
- case MSG_RUN_ADB_RESTORE:
- {
- // TODO: refactor full restore to be a looper-based state machine
- // similar to normal backup/restore.
- AdbRestoreParams params = (AdbRestoreParams)msg.obj;
- PerformAdbRestoreTask task = new PerformAdbRestoreTask(params.fd,
- params.curPassword, params.encryptPassword,
- params.observer, params.latch);
- (new Thread(task, "adb-restore")).start();
- break;
- }
-
- case MSG_RUN_CLEAR:
- {
- ClearParams params = (ClearParams)msg.obj;
- (new PerformClearTask(params.transport, params.packageInfo)).run();
- break;
- }
-
- case MSG_RETRY_CLEAR:
- {
- // reenqueues if the transport remains unavailable
- ClearRetryParams params = (ClearRetryParams)msg.obj;
- clearBackupData(params.transportName, params.packageName);
- break;
- }
-
- case MSG_RETRY_INIT:
- {
- synchronized (mQueueLock) {
- recordInitPendingLocked(msg.arg1 != 0, (String)msg.obj);
- mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
- mRunInitIntent);
- }
- break;
- }
-
- case MSG_RUN_GET_RESTORE_SETS:
- {
- // Like other async operations, this is entered with the wakelock held
- RestoreSet[] sets = null;
- RestoreGetSetsParams params = (RestoreGetSetsParams)msg.obj;
- try {
- sets = params.transport.getAvailableRestoreSets();
- // cache the result in the active session
- synchronized (params.session) {
- params.session.mRestoreSets = sets;
- }
- if (sets == null) EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
- } catch (Exception e) {
- Slog.e(TAG, "Error from transport getting set list: " + e.getMessage());
- } finally {
- if (params.observer != null) {
- try {
- params.observer.restoreSetsAvailable(sets);
- } catch (RemoteException re) {
- Slog.e(TAG, "Unable to report listing to observer");
- } catch (Exception e) {
- Slog.e(TAG, "Restore observer threw: " + e.getMessage());
- }
- }
-
- // Done: reset the session timeout clock
- removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
- sendEmptyMessageDelayed(MSG_RESTORE_SESSION_TIMEOUT, TIMEOUT_RESTORE_INTERVAL);
-
- mWakelock.release();
- }
- break;
- }
-
- case MSG_BACKUP_OPERATION_TIMEOUT:
- case MSG_RESTORE_OPERATION_TIMEOUT:
- {
- Slog.d(TAG, "Timeout message received for token=" + Integer.toHexString(msg.arg1));
- handleCancel(msg.arg1, false);
- break;
- }
-
- case MSG_RESTORE_SESSION_TIMEOUT:
- {
- synchronized (BackupManagerService.this) {
- if (mActiveRestoreSession != null) {
- // Client app left the restore session dangling. We know that it
- // can't be in the middle of an actual restore operation because
- // the timeout is suspended while a restore is in progress. Clean
- // up now.
- Slog.w(TAG, "Restore session timed out; aborting");
- mActiveRestoreSession.markTimedOut();
- post(mActiveRestoreSession.new EndRestoreRunnable(
- BackupManagerService.this, mActiveRestoreSession));
- }
- }
- break;
- }
-
- case MSG_FULL_CONFIRMATION_TIMEOUT:
- {
- synchronized (mAdbBackupRestoreConfirmations) {
- AdbParams params = mAdbBackupRestoreConfirmations.get(msg.arg1);
- if (params != null) {
- Slog.i(TAG, "Full backup/restore timed out waiting for user confirmation");
-
- // Release the waiter; timeout == completion
- signalAdbBackupRestoreCompletion(params);
-
- // Remove the token from the set
- mAdbBackupRestoreConfirmations.delete(msg.arg1);
-
- // Report a timeout to the observer, if any
- if (params.observer != null) {
- try {
- params.observer.onTimeout();
- } catch (RemoteException e) {
- /* don't care if the app has gone away */
- }
- }
- } else {
- Slog.d(TAG, "couldn't find params for token " + msg.arg1);
- }
- }
- break;
- }
-
- case MSG_WIDGET_BROADCAST:
- {
- final Intent intent = (Intent) msg.obj;
- mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
- break;
- }
-
- case MSG_REQUEST_BACKUP:
- {
- BackupParams params = (BackupParams)msg.obj;
- if (MORE_DEBUG) {
- Slog.d(TAG, "MSG_REQUEST_BACKUP observer=" + params.observer);
- }
- ArrayList<BackupRequest> kvQueue = new ArrayList<>();
- for (String packageName : params.kvPackages) {
- kvQueue.add(new BackupRequest(packageName));
- }
- mBackupRunning = true;
- mWakelock.acquire();
-
- PerformBackupTask pbt = new PerformBackupTask(params.transport, params.dirName,
- kvQueue, null, params.observer, params.monitor, params.fullPackages, true,
- params.nonIncrementalBackup);
- Message pbtMessage = obtainMessage(MSG_BACKUP_RESTORE_STEP, pbt);
- sendMessage(pbtMessage);
- break;
- }
-
- case MSG_SCHEDULE_BACKUP_PACKAGE:
- {
- String pkgName = (String)msg.obj;
- if (MORE_DEBUG) {
- Slog.d(TAG, "MSG_SCHEDULE_BACKUP_PACKAGE " + pkgName);
- }
- dataChangedImpl(pkgName);
- break;
- }
- }
- }
- }
-
- // ----- Debug-only backup operation trace -----
- void addBackupTrace(String s) {
- if (DEBUG_BACKUP_TRACE) {
- synchronized (mBackupTrace) {
- mBackupTrace.add(s);
- }
- }
- }
-
- void clearBackupTrace() {
- if (DEBUG_BACKUP_TRACE) {
- synchronized (mBackupTrace) {
- mBackupTrace.clear();
- }
- }
- }
-
- // ----- Main service implementation -----
-
- public BackupManagerService(Context context, Trampoline parent, HandlerThread backupThread) {
- mContext = context;
- mPackageManager = context.getPackageManager();
- mPackageManagerBinder = AppGlobals.getPackageManager();
- mActivityManager = ActivityManager.getService();
-
- mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- mStorageManager = IStorageManager.Stub.asInterface(ServiceManager.getService("mount"));
-
- mBackupManagerBinder = Trampoline.asInterface(parent.asBinder());
-
- // spin up the backup/restore handler thread
- mBackupHandler = new BackupHandler(backupThread.getLooper());
-
- // Set up our bookkeeping
- final ContentResolver resolver = context.getContentResolver();
- mProvisioned = Settings.Global.getInt(resolver,
- Settings.Global.DEVICE_PROVISIONED, 0) != 0;
- mAutoRestore = Settings.Secure.getInt(resolver,
- Settings.Secure.BACKUP_AUTO_RESTORE, 1) != 0;
-
- mProvisionedObserver = new ProvisionedObserver(mBackupHandler);
- resolver.registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
- false, mProvisionedObserver);
-
- // If Encrypted file systems is enabled or disabled, this call will return the
- // correct directory.
- mBaseStateDir = new File(Environment.getDataDirectory(), "backup");
- mBaseStateDir.mkdirs();
- if (!SELinux.restorecon(mBaseStateDir)) {
- Slog.e(TAG, "SELinux restorecon failed on " + mBaseStateDir);
- }
-
- // This dir on /cache is managed directly in init.rc
- mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup_stage");
-
- mPasswordVersion = 1; // unless we hear otherwise
- mPasswordVersionFile = new File(mBaseStateDir, "pwversion");
- if (mPasswordVersionFile.exists()) {
- FileInputStream fin = null;
- DataInputStream in = null;
- try {
- fin = new FileInputStream(mPasswordVersionFile);
- in = new DataInputStream(fin);
- mPasswordVersion = in.readInt();
- } catch (IOException e) {
- Slog.e(TAG, "Unable to read backup pw version");
- } finally {
- try {
- if (in != null) in.close();
- if (fin != null) fin.close();
- } catch (IOException e) {
- Slog.w(TAG, "Error closing pw version files");
- }
- }
- }
-
- mPasswordHashFile = new File(mBaseStateDir, "pwhash");
- if (mPasswordHashFile.exists()) {
- FileInputStream fin = null;
- DataInputStream in = null;
- try {
- fin = new FileInputStream(mPasswordHashFile);
- in = new DataInputStream(new BufferedInputStream(fin));
- // integer length of the salt array, followed by the salt,
- // then the hex pw hash string
- int saltLen = in.readInt();
- byte[] salt = new byte[saltLen];
- in.readFully(salt);
- mPasswordHash = in.readUTF();
- mPasswordSalt = salt;
- } catch (IOException e) {
- Slog.e(TAG, "Unable to read saved backup pw hash");
- } finally {
- try {
- if (in != null) in.close();
- if (fin != null) fin.close();
- } catch (IOException e) {
- Slog.w(TAG, "Unable to close streams");
- }
- }
- }
-
- // Alarm receivers for scheduled backups & initialization operations
- mRunBackupReceiver = new RunBackupReceiver();
- IntentFilter filter = new IntentFilter();
- filter.addAction(RUN_BACKUP_ACTION);
- context.registerReceiver(mRunBackupReceiver, filter,
- android.Manifest.permission.BACKUP, null);
-
- mRunInitReceiver = new RunInitializeReceiver();
- filter = new IntentFilter();
- filter.addAction(RUN_INITIALIZE_ACTION);
- context.registerReceiver(mRunInitReceiver, filter,
- android.Manifest.permission.BACKUP, null);
-
- Intent backupIntent = new Intent(RUN_BACKUP_ACTION);
- backupIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- mRunBackupIntent = PendingIntent.getBroadcast(context, MSG_RUN_BACKUP, backupIntent, 0);
-
- Intent initIntent = new Intent(RUN_INITIALIZE_ACTION);
- initIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- mRunInitIntent = PendingIntent.getBroadcast(context, 0, initIntent, 0);
-
- // Set up the backup-request journaling
- mJournalDir = new File(mBaseStateDir, "pending");
- mJournalDir.mkdirs(); // creates mBaseStateDir along the way
- mJournal = null; // will be created on first use
-
- mConstants = new BackupManagerConstants(mBackupHandler, mContext.getContentResolver());
-
- // Set up the various sorts of package tracking we do
- mFullBackupScheduleFile = new File(mBaseStateDir, "fb-schedule");
- initPackageTracking();
-
- // Build our mapping of uid to backup client services. This implicitly
- // schedules a backup pass on the Package Manager metadata the first
- // time anything needs to be backed up.
- synchronized (mBackupParticipants) {
- addPackageParticipantsLocked(null);
- }
-
- // Set up our transport options and initialize the default transport
- // TODO: Don't create transports that we don't need to?
- SystemConfig systemConfig = SystemConfig.getInstance();
- Set<ComponentName> transportWhitelist = systemConfig.getBackupTransportWhitelist();
-
- String transport = Settings.Secure.getString(context.getContentResolver(),
- Settings.Secure.BACKUP_TRANSPORT);
- if (TextUtils.isEmpty(transport)) {
- transport = null;
- }
- String currentTransport = transport;
- if (DEBUG) Slog.v(TAG, "Starting with transport " + currentTransport);
-
- mTransportManager = new TransportManager(context, transportWhitelist, currentTransport,
- mTransportBoundListener, backupThread.getLooper());
- mTransportManager.registerAllTransports();
-
- // Now that we know about valid backup participants, parse any
- // leftover journal files into the pending backup set
- mBackupHandler.post(() -> parseLeftoverJournals());
-
- // Power management
- mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*");
- }
-
- private class RunBackupReceiver extends BroadcastReceiver {
- public void onReceive(Context context, Intent intent) {
- if (RUN_BACKUP_ACTION.equals(intent.getAction())) {
- synchronized (mQueueLock) {
- if (mPendingInits.size() > 0) {
- // If there are pending init operations, we process those
- // and then settle into the usual periodic backup schedule.
- if (MORE_DEBUG) Slog.v(TAG, "Init pending at scheduled backup");
- try {
- mAlarmManager.cancel(mRunInitIntent);
- mRunInitIntent.send();
- } catch (PendingIntent.CanceledException ce) {
- Slog.e(TAG, "Run init intent cancelled");
- // can't really do more than bail here
- }
- } else {
- // Don't run backups now if we're disabled or not yet
- // fully set up.
- if (mEnabled && mProvisioned) {
- if (!mBackupRunning) {
- if (DEBUG) Slog.v(TAG, "Running a backup pass");
-
- // Acquire the wakelock and pass it to the backup thread. it will
- // be released once backup concludes.
- mBackupRunning = true;
- mWakelock.acquire();
-
- Message msg = mBackupHandler.obtainMessage(MSG_RUN_BACKUP);
- mBackupHandler.sendMessage(msg);
- } else {
- Slog.i(TAG, "Backup time but one already running");
- }
- } else {
- Slog.w(TAG, "Backup pass but e=" + mEnabled + " p=" + mProvisioned);
- }
- }
- }
- }
- }
- }
-
- private class RunInitializeReceiver extends BroadcastReceiver {
- public void onReceive(Context context, Intent intent) {
- if (RUN_INITIALIZE_ACTION.equals(intent.getAction())) {
- // Snapshot the pending-init queue and work on that
- synchronized (mQueueLock) {
- String[] queue = mPendingInits.toArray(new String[mPendingInits.size()]);
- mPendingInits.clear();
-
- // Acquire the wakelock and pass it to the init thread. it will
- // be released once init concludes.
- mWakelock.acquire();
- mBackupHandler.post(new PerformInitializeTask(queue, null));
- }
- }
- }
- }
-
- private void initPackageTracking() {
- if (MORE_DEBUG) Slog.v(TAG, "` tracking");
-
- // Remember our ancestral dataset
- mTokenFile = new File(mBaseStateDir, "ancestral");
- try (DataInputStream tokenStream = new DataInputStream(new BufferedInputStream(
- new FileInputStream(mTokenFile)))) {
- int version = tokenStream.readInt();
- if (version == CURRENT_ANCESTRAL_RECORD_VERSION) {
- mAncestralToken = tokenStream.readLong();
- mCurrentToken = tokenStream.readLong();
-
- int numPackages = tokenStream.readInt();
- if (numPackages >= 0) {
- mAncestralPackages = new HashSet<>();
- for (int i = 0; i < numPackages; i++) {
- String pkgName = tokenStream.readUTF();
- mAncestralPackages.add(pkgName);
- }
- }
- }
- } catch (FileNotFoundException fnf) {
- // Probably innocuous
- Slog.v(TAG, "No ancestral data");
- } catch (IOException e) {
- Slog.w(TAG, "Unable to read token file", e);
- }
-
- // Keep a log of what apps we've ever backed up. Because we might have
- // rebooted in the middle of an operation that was removing something from
- // this log, we sanity-check its contents here and reconstruct it.
- mEverStored = new File(mBaseStateDir, "processed");
-
- // If there are previous contents, parse them out then start a new
- // file to continue the recordkeeping.
- if (mEverStored.exists()) {
- try (DataInputStream in = new DataInputStream(
- new BufferedInputStream(new FileInputStream(mEverStored)))) {
-
- // Loop until we hit EOF
- while (true) {
- String pkg = in.readUTF();
- mEverStoredApps.add(pkg);
- if (MORE_DEBUG) Slog.v(TAG, " + " + pkg);
- }
- } catch (EOFException e) {
- // Done
- } catch (IOException e) {
- Slog.e(TAG, "Error in processed file", e);
- }
- }
-
- synchronized (mQueueLock) {
- // Resume the full-data backup queue
- mFullBackupQueue = readFullBackupSchedule();
- }
-
- // Register for broadcasts about package install, etc., so we can
- // update the provider list.
- IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_PACKAGE_ADDED);
- filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
- filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
- filter.addDataScheme("package");
- mContext.registerReceiver(mBroadcastReceiver, filter);
- // Register for events related to sdcard installation.
- IntentFilter sdFilter = new IntentFilter();
- sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
- sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
- mContext.registerReceiver(mBroadcastReceiver, sdFilter);
- }
-
- private ArrayList<FullBackupEntry> readFullBackupSchedule() {
- boolean changed = false;
- ArrayList<FullBackupEntry> schedule = null;
- List<PackageInfo> apps =
- PackageManagerBackupAgent.getStorableApplications(mPackageManager);
-
- if (mFullBackupScheduleFile.exists()) {
- FileInputStream fstream = null;
- BufferedInputStream bufStream = null;
- DataInputStream in = null;
- try {
- fstream = new FileInputStream(mFullBackupScheduleFile);
- bufStream = new BufferedInputStream(fstream);
- in = new DataInputStream(bufStream);
-
- int version = in.readInt();
- if (version != SCHEDULE_FILE_VERSION) {
- Slog.e(TAG, "Unknown backup schedule version " + version);
- return null;
- }
-
- final int N = in.readInt();
- schedule = new ArrayList<FullBackupEntry>(N);
-
- // HashSet instead of ArraySet specifically because we want the eventual
- // lookups against O(hundreds) of entries to be as fast as possible, and
- // we discard the set immediately after the scan so the extra memory
- // overhead is transient.
- HashSet<String> foundApps = new HashSet<String>(N);
-
- for (int i = 0; i < N; i++) {
- String pkgName = in.readUTF();
- long lastBackup = in.readLong();
- foundApps.add(pkgName); // all apps that we've addressed already
- try {
- PackageInfo pkg = mPackageManager.getPackageInfo(pkgName, 0);
- if (appGetsFullBackup(pkg)
- && appIsEligibleForBackup(pkg.applicationInfo, mPackageManager)) {
- schedule.add(new FullBackupEntry(pkgName, lastBackup));
- } else {
- if (DEBUG) {
- Slog.i(TAG, "Package " + pkgName
- + " no longer eligible for full backup");
- }
- }
- } catch (NameNotFoundException e) {
- if (DEBUG) {
- Slog.i(TAG, "Package " + pkgName
- + " not installed; dropping from full backup");
- }
- }
- }
-
- // New apps can arrive "out of band" via OTA and similar, so we also need to
- // scan to make sure that we're tracking all full-backup candidates properly
- for (PackageInfo app : apps) {
- if (appGetsFullBackup(app)
- && appIsEligibleForBackup(app.applicationInfo, mPackageManager)) {
- if (!foundApps.contains(app.packageName)) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "New full backup app " + app.packageName + " found");
- }
- schedule.add(new FullBackupEntry(app.packageName, 0));
- changed = true;
- }
- }
- }
-
- Collections.sort(schedule);
- } catch (Exception e) {
- Slog.e(TAG, "Unable to read backup schedule", e);
- mFullBackupScheduleFile.delete();
- schedule = null;
- } finally {
- IoUtils.closeQuietly(in);
- IoUtils.closeQuietly(bufStream);
- IoUtils.closeQuietly(fstream);
- }
- }
-
- if (schedule == null) {
- // no prior queue record, or unable to read it. Set up the queue
- // from scratch.
- changed = true;
- schedule = new ArrayList<FullBackupEntry>(apps.size());
- for (PackageInfo info : apps) {
- if (appGetsFullBackup(info)
- && appIsEligibleForBackup(info.applicationInfo, mPackageManager)) {
- schedule.add(new FullBackupEntry(info.packageName, 0));
- }
- }
- }
-
- if (changed) {
- writeFullBackupScheduleAsync();
- }
- return schedule;
- }
-
- Runnable mFullBackupScheduleWriter = new Runnable() {
- @Override public void run() {
- synchronized (mQueueLock) {
- try {
- ByteArrayOutputStream bufStream = new ByteArrayOutputStream(4096);
- DataOutputStream bufOut = new DataOutputStream(bufStream);
- bufOut.writeInt(SCHEDULE_FILE_VERSION);
-
- // version 1:
- //
- // [int] # of packages in the queue = N
- // N * {
- // [utf8] package name
- // [long] last backup time for this package
- // }
- int N = mFullBackupQueue.size();
- bufOut.writeInt(N);
-
- for (int i = 0; i < N; i++) {
- FullBackupEntry entry = mFullBackupQueue.get(i);
- bufOut.writeUTF(entry.packageName);
- bufOut.writeLong(entry.lastBackup);
- }
- bufOut.flush();
-
- AtomicFile af = new AtomicFile(mFullBackupScheduleFile);
- FileOutputStream out = af.startWrite();
- out.write(bufStream.toByteArray());
- af.finishWrite(out);
- } catch (Exception e) {
- Slog.e(TAG, "Unable to write backup schedule!", e);
- }
- }
- }
- };
-
- private void writeFullBackupScheduleAsync() {
- mBackupHandler.removeCallbacks(mFullBackupScheduleWriter);
- mBackupHandler.post(mFullBackupScheduleWriter);
- }
-
- private void parseLeftoverJournals() {
- for (File f : mJournalDir.listFiles()) {
- if (mJournal == null || f.compareTo(mJournal) != 0) {
- // This isn't the current journal, so it must be a leftover. Read
- // out the package names mentioned there and schedule them for
- // backup.
- DataInputStream in = null;
- try {
- Slog.i(TAG, "Found stale backup journal, scheduling");
- // Journals will tend to be on the order of a few kilobytes(around 4k), hence,
- // setting the buffer size to 8192.
- InputStream bufferedInputStream = new BufferedInputStream(
- new FileInputStream(f), 8192);
- in = new DataInputStream(bufferedInputStream);
- while (true) {
- String packageName = in.readUTF();
- if (MORE_DEBUG) Slog.i(TAG, " " + packageName);
- dataChangedImpl(packageName);
- }
- } catch (EOFException e) {
- // no more data; we're done
- } catch (Exception e) {
- Slog.e(TAG, "Can't read " + f, e);
- } finally {
- // close/delete the file
- try { if (in != null) in.close(); } catch (IOException e) {}
- f.delete();
- }
- }
- }
- }
-
- private SecretKey buildPasswordKey(String algorithm, String pw, byte[] salt, int rounds) {
- return buildCharArrayKey(algorithm, pw.toCharArray(), salt, rounds);
- }
-
- private SecretKey buildCharArrayKey(String algorithm, char[] pwArray, byte[] salt, int rounds) {
- try {
- SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
- KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE);
- return keyFactory.generateSecret(ks);
- } catch (InvalidKeySpecException e) {
- Slog.e(TAG, "Invalid key spec for PBKDF2!");
- } catch (NoSuchAlgorithmException e) {
- Slog.e(TAG, "PBKDF2 unavailable!");
- }
- return null;
- }
-
- private String buildPasswordHash(String algorithm, String pw, byte[] salt, int rounds) {
- SecretKey key = buildPasswordKey(algorithm, pw, salt, rounds);
- if (key != null) {
- return byteArrayToHex(key.getEncoded());
- }
- return null;
- }
-
- private String byteArrayToHex(byte[] data) {
- StringBuilder buf = new StringBuilder(data.length * 2);
- for (int i = 0; i < data.length; i++) {
- buf.append(Byte.toHexString(data[i], true));
- }
- return buf.toString();
- }
-
- private byte[] hexToByteArray(String digits) {
- final int bytes = digits.length() / 2;
- if (2*bytes != digits.length()) {
- throw new IllegalArgumentException("Hex string must have an even number of digits");
- }
-
- byte[] result = new byte[bytes];
- for (int i = 0; i < digits.length(); i += 2) {
- result[i/2] = (byte) Integer.parseInt(digits.substring(i, i+2), 16);
- }
- return result;
- }
-
- private byte[] makeKeyChecksum(String algorithm, byte[] pwBytes, byte[] salt, int rounds) {
- char[] mkAsChar = new char[pwBytes.length];
- for (int i = 0; i < pwBytes.length; i++) {
- mkAsChar[i] = (char) pwBytes[i];
- }
-
- Key checksum = buildCharArrayKey(algorithm, mkAsChar, salt, rounds);
- return checksum.getEncoded();
- }
-
- // Used for generating random salts or passwords
- private byte[] randomBytes(int bits) {
- byte[] array = new byte[bits / 8];
- mRng.nextBytes(array);
- return array;
- }
-
- boolean passwordMatchesSaved(String algorithm, String candidatePw, int rounds) {
- if (mPasswordHash == null) {
- // no current password case -- require that 'currentPw' be null or empty
- if (candidatePw == null || "".equals(candidatePw)) {
- return true;
- } // else the non-empty candidate does not match the empty stored pw
- } else {
- // hash the stated current pw and compare to the stored one
- if (candidatePw != null && candidatePw.length() > 0) {
- String currentPwHash = buildPasswordHash(algorithm, candidatePw, mPasswordSalt, rounds);
- if (mPasswordHash.equalsIgnoreCase(currentPwHash)) {
- // candidate hash matches the stored hash -- the password matches
- return true;
- }
- } // else the stored pw is nonempty but the candidate is empty; no match
- }
- return false;
- }
-
- @Override
- public boolean setBackupPassword(String currentPw, String newPw) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "setBackupPassword");
-
- // When processing v1 passwords we may need to try two different PBKDF2 checksum regimes
- final boolean pbkdf2Fallback = (mPasswordVersion < BACKUP_PW_FILE_VERSION);
-
- // If the supplied pw doesn't hash to the the saved one, fail. The password
- // might be caught in the legacy crypto mismatch; verify that too.
- if (!passwordMatchesSaved(PBKDF_CURRENT, currentPw, PBKDF2_HASH_ROUNDS)
- && !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK,
- currentPw, PBKDF2_HASH_ROUNDS))) {
- return false;
- }
-
- // Snap up to current on the pw file version
- mPasswordVersion = BACKUP_PW_FILE_VERSION;
- FileOutputStream pwFout = null;
- DataOutputStream pwOut = null;
- try {
- pwFout = new FileOutputStream(mPasswordVersionFile);
- pwOut = new DataOutputStream(pwFout);
- pwOut.writeInt(mPasswordVersion);
- } catch (IOException e) {
- Slog.e(TAG, "Unable to write backup pw version; password not changed");
- return false;
- } finally {
- try {
- if (pwOut != null) pwOut.close();
- if (pwFout != null) pwFout.close();
- } catch (IOException e) {
- Slog.w(TAG, "Unable to close pw version record");
- }
- }
-
- // Clearing the password is okay
- if (newPw == null || newPw.isEmpty()) {
- if (mPasswordHashFile.exists()) {
- if (!mPasswordHashFile.delete()) {
- // Unable to delete the old pw file, so fail
- Slog.e(TAG, "Unable to clear backup password");
- return false;
- }
- }
- mPasswordHash = null;
- mPasswordSalt = null;
- return true;
- }
-
- try {
- // Okay, build the hash of the new backup password
- byte[] salt = randomBytes(PBKDF2_SALT_SIZE);
- String newPwHash = buildPasswordHash(PBKDF_CURRENT, newPw, salt, PBKDF2_HASH_ROUNDS);
-
- OutputStream pwf = null, buffer = null;
- DataOutputStream out = null;
- try {
- pwf = new FileOutputStream(mPasswordHashFile);
- buffer = new BufferedOutputStream(pwf);
- out = new DataOutputStream(buffer);
- // integer length of the salt array, followed by the salt,
- // then the hex pw hash string
- out.writeInt(salt.length);
- out.write(salt);
- out.writeUTF(newPwHash);
- out.flush();
- mPasswordHash = newPwHash;
- mPasswordSalt = salt;
- return true;
- } finally {
- if (out != null) out.close();
- if (buffer != null) buffer.close();
- if (pwf != null) pwf.close();
- }
- } catch (IOException e) {
- Slog.e(TAG, "Unable to set backup password");
- }
- return false;
- }
-
- @Override
- public boolean hasBackupPassword() {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "hasBackupPassword");
-
- return mPasswordHash != null && mPasswordHash.length() > 0;
- }
-
- private boolean backupPasswordMatches(String currentPw) {
- if (hasBackupPassword()) {
- final boolean pbkdf2Fallback = (mPasswordVersion < BACKUP_PW_FILE_VERSION);
- if (!passwordMatchesSaved(PBKDF_CURRENT, currentPw, PBKDF2_HASH_ROUNDS)
- && !(pbkdf2Fallback && passwordMatchesSaved(PBKDF_FALLBACK,
- currentPw, PBKDF2_HASH_ROUNDS))) {
- if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
- return false;
- }
- }
- return true;
- }
-
- // Maintain persistent state around whether need to do an initialize operation.
- // Must be called with the queue lock held.
- void recordInitPendingLocked(boolean isPending, String transportName) {
- if (MORE_DEBUG) Slog.i(TAG, "recordInitPendingLocked: " + isPending
- + " on transport " + transportName);
- mBackupHandler.removeMessages(MSG_RETRY_INIT);
-
- try {
- IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
- if (transport != null) {
- String transportDirName = transport.transportDirName();
- File stateDir = new File(mBaseStateDir, transportDirName);
- File initPendingFile = new File(stateDir, INIT_SENTINEL_FILE_NAME);
-
- if (isPending) {
- // We need an init before we can proceed with sending backup data.
- // Record that with an entry in our set of pending inits, as well as
- // journaling it via creation of a sentinel file.
- mPendingInits.add(transportName);
- try {
- (new FileOutputStream(initPendingFile)).close();
- } catch (IOException ioe) {
- // Something is badly wrong with our permissions; just try to move on
- }
- } else {
- // No more initialization needed; wipe the journal and reset our state.
- initPendingFile.delete();
- mPendingInits.remove(transportName);
- }
- return; // done; don't fall through to the error case
- }
- } catch (Exception e) {
- // transport threw when asked its name; fall through to the lookup-failed case
- Slog.e(TAG, "Transport " + transportName + " failed to report name: "
- + e.getMessage());
- }
-
- // The named transport doesn't exist or threw. This operation is
- // important, so we record the need for a an init and post a message
- // to retry the init later.
- if (isPending) {
- mPendingInits.add(transportName);
- mBackupHandler.sendMessageDelayed(
- mBackupHandler.obtainMessage(MSG_RETRY_INIT,
- (isPending ? 1 : 0),
- 0,
- transportName),
- TRANSPORT_RETRY_INTERVAL);
- }
- }
-
- // Reset all of our bookkeeping, in response to having been told that
- // the backend data has been wiped [due to idle expiry, for example],
- // so we must re-upload all saved settings.
- void resetBackupState(File stateFileDir) {
- synchronized (mQueueLock) {
- // Wipe the "what we've ever backed up" tracking
- mEverStoredApps.clear();
- mEverStored.delete();
-
- mCurrentToken = 0;
- writeRestoreTokens();
-
- // Remove all the state files
- for (File sf : stateFileDir.listFiles()) {
- // ... but don't touch the needs-init sentinel
- if (!sf.getName().equals(INIT_SENTINEL_FILE_NAME)) {
- sf.delete();
- }
- }
- }
-
- // Enqueue a new backup of every participant
- synchronized (mBackupParticipants) {
- final int N = mBackupParticipants.size();
- for (int i=0; i<N; i++) {
- HashSet<String> participants = mBackupParticipants.valueAt(i);
- if (participants != null) {
- for (String packageName : participants) {
- dataChangedImpl(packageName);
- }
- }
- }
- }
- }
-
- private TransportManager.TransportBoundListener mTransportBoundListener =
- new TransportManager.TransportBoundListener() {
- @Override
- public boolean onTransportBound(IBackupTransport transport) {
- // If the init sentinel file exists, we need to be sure to perform the init
- // as soon as practical. We also create the state directory at registration
- // time to ensure it's present from the outset.
- String name = null;
- try {
- name = transport.name();
- String transportDirName = transport.transportDirName();
- File stateDir = new File(mBaseStateDir, transportDirName);
- stateDir.mkdirs();
-
- File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME);
- if (initSentinel.exists()) {
- synchronized (mQueueLock) {
- mPendingInits.add(name);
-
- // TODO: pick a better starting time than now + 1 minute
- long delay = 1000 * 60; // one minute, in milliseconds
- mAlarmManager.set(AlarmManager.RTC_WAKEUP,
- System.currentTimeMillis() + delay, mRunInitIntent);
- }
- }
- return true;
- } catch (Exception e) {
- // the transport threw when asked its file naming prefs; declare it invalid
- Slog.w(TAG, "Failed to regiser transport: " + name);
- return false;
- }
- }
- };
-
- // ----- Track installation/removal of packages -----
- BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
- public void onReceive(Context context, Intent intent) {
- if (MORE_DEBUG) Slog.d(TAG, "Received broadcast " + intent);
-
- String action = intent.getAction();
- boolean replacing = false;
- boolean added = false;
- boolean changed = false;
- Bundle extras = intent.getExtras();
- String pkgList[] = null;
- if (Intent.ACTION_PACKAGE_ADDED.equals(action) ||
- Intent.ACTION_PACKAGE_REMOVED.equals(action) ||
- Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
- Uri uri = intent.getData();
- if (uri == null) {
- return;
- }
- final String pkgName = uri.getSchemeSpecificPart();
- if (pkgName != null) {
- pkgList = new String[] { pkgName };
- }
- changed = Intent.ACTION_PACKAGE_CHANGED.equals(action);
-
- // At package-changed we only care about looking at new transport states
- if (changed) {
- final String[] components =
- intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
-
- if (MORE_DEBUG) {
- Slog.i(TAG, "Package " + pkgName + " changed; rechecking");
- for (int i = 0; i < components.length; i++) {
- Slog.i(TAG, " * " + components[i]);
- }
- }
-
- mBackupHandler.post(
- () -> mTransportManager.onPackageChanged(pkgName, components));
- return; // nothing more to do in the PACKAGE_CHANGED case
- }
-
- added = Intent.ACTION_PACKAGE_ADDED.equals(action);
- replacing = extras.getBoolean(Intent.EXTRA_REPLACING, false);
- } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
- added = true;
- pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
- } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
- added = false;
- pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
- }
-
- if (pkgList == null || pkgList.length == 0) {
- return;
- }
-
- final int uid = extras.getInt(Intent.EXTRA_UID);
- if (added) {
- synchronized (mBackupParticipants) {
- if (replacing) {
- // This is the package-replaced case; we just remove the entry
- // under the old uid and fall through to re-add. If an app
- // just added key/value backup participation, this picks it up
- // as a known participant.
- removePackageParticipantsLocked(pkgList, uid);
- }
- addPackageParticipantsLocked(pkgList);
- }
- // If they're full-backup candidates, add them there instead
- final long now = System.currentTimeMillis();
- for (final String packageName : pkgList) {
- try {
- PackageInfo app = mPackageManager.getPackageInfo(packageName, 0);
- if (appGetsFullBackup(app)
- && appIsEligibleForBackup(app.applicationInfo, mPackageManager)) {
- enqueueFullBackup(packageName, now);
- scheduleNextFullBackupJob(0);
- } else {
- // The app might have just transitioned out of full-data into
- // doing key/value backups, or might have just disabled backups
- // entirely. Make sure it is no longer in the full-data queue.
- synchronized (mQueueLock) {
- dequeueFullBackupLocked(packageName);
- }
- writeFullBackupScheduleAsync();
- }
-
- mBackupHandler.post(
- () -> mTransportManager.onPackageAdded(packageName));
-
- } catch (NameNotFoundException e) {
- // doesn't really exist; ignore it
- if (DEBUG) {
- Slog.w(TAG, "Can't resolve new app " + packageName);
- }
- }
- }
-
- // Whenever a package is added or updated we need to update
- // the package metadata bookkeeping.
- dataChangedImpl(PACKAGE_MANAGER_SENTINEL);
- } else {
- if (replacing) {
- // The package is being updated. We'll receive a PACKAGE_ADDED shortly.
- } else {
- // Outright removal. In the full-data case, the app will be dropped
- // from the queue when its (now obsolete) name comes up again for
- // backup.
- synchronized (mBackupParticipants) {
- removePackageParticipantsLocked(pkgList, uid);
- }
- }
- for (final String pkgName : pkgList) {
- mBackupHandler.post(
- () -> mTransportManager.onPackageRemoved(pkgName));
- }
- }
- }
- };
-
- // Add the backup agents in the given packages to our set of known backup participants.
- // If 'packageNames' is null, adds all backup agents in the whole system.
- void addPackageParticipantsLocked(String[] packageNames) {
- // Look for apps that define the android:backupAgent attribute
- List<PackageInfo> targetApps = allAgentPackages();
- if (packageNames != null) {
- if (MORE_DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: #" + packageNames.length);
- for (String packageName : packageNames) {
- addPackageParticipantsLockedInner(packageName, targetApps);
- }
- } else {
- if (MORE_DEBUG) Slog.v(TAG, "addPackageParticipantsLocked: all");
- addPackageParticipantsLockedInner(null, targetApps);
- }
- }
-
- private void addPackageParticipantsLockedInner(String packageName,
- List<PackageInfo> targetPkgs) {
- if (MORE_DEBUG) {
- Slog.v(TAG, "Examining " + packageName + " for backup agent");
- }
-
- for (PackageInfo pkg : targetPkgs) {
- if (packageName == null || pkg.packageName.equals(packageName)) {
- int uid = pkg.applicationInfo.uid;
- HashSet<String> set = mBackupParticipants.get(uid);
- if (set == null) {
- set = new HashSet<>();
- mBackupParticipants.put(uid, set);
- }
- set.add(pkg.packageName);
- if (MORE_DEBUG) Slog.v(TAG, "Agent found; added");
-
- // Schedule a backup for it on general principles
- if (MORE_DEBUG) Slog.i(TAG, "Scheduling backup for new app " + pkg.packageName);
- Message msg = mBackupHandler
- .obtainMessage(MSG_SCHEDULE_BACKUP_PACKAGE, pkg.packageName);
- mBackupHandler.sendMessage(msg);
- }
- }
- }
-
- // Remove the given packages' entries from our known active set.
- void removePackageParticipantsLocked(String[] packageNames, int oldUid) {
- if (packageNames == null) {
- Slog.w(TAG, "removePackageParticipants with null list");
- return;
- }
-
- if (MORE_DEBUG) Slog.v(TAG, "removePackageParticipantsLocked: uid=" + oldUid
- + " #" + packageNames.length);
- for (String pkg : packageNames) {
- // Known previous UID, so we know which package set to check
- HashSet<String> set = mBackupParticipants.get(oldUid);
- if (set != null && set.contains(pkg)) {
- removePackageFromSetLocked(set, pkg);
- if (set.isEmpty()) {
- if (MORE_DEBUG) Slog.v(TAG, " last one of this uid; purging set");
- mBackupParticipants.remove(oldUid);
- }
- }
- }
- }
-
- private void removePackageFromSetLocked(final HashSet<String> set,
- final String packageName) {
- if (set.contains(packageName)) {
- // Found it. Remove this one package from the bookkeeping, and
- // if it's the last participating app under this uid we drop the
- // (now-empty) set as well.
- // Note that we deliberately leave it 'known' in the "ever backed up"
- // bookkeeping so that its current-dataset data will be retrieved
- // if the app is subsequently reinstalled
- if (MORE_DEBUG) Slog.v(TAG, " removing participant " + packageName);
- set.remove(packageName);
- mPendingBackups.remove(packageName);
- }
- }
-
- // Returns the set of all applications that define an android:backupAgent attribute
- List<PackageInfo> allAgentPackages() {
- // !!! TODO: cache this and regenerate only when necessary
- int flags = PackageManager.GET_SIGNATURES;
- List<PackageInfo> packages = mPackageManager.getInstalledPackages(flags);
- int N = packages.size();
- for (int a = N-1; a >= 0; a--) {
- PackageInfo pkg = packages.get(a);
- try {
- ApplicationInfo app = pkg.applicationInfo;
- if (((app.flags&ApplicationInfo.FLAG_ALLOW_BACKUP) == 0)
- || app.backupAgentName == null
- || (app.flags&ApplicationInfo.FLAG_FULL_BACKUP_ONLY) != 0) {
- packages.remove(a);
- }
- else {
- // we will need the shared library path, so look that up and store it here.
- // This is used implicitly when we pass the PackageInfo object off to
- // the Activity Manager to launch the app for backup/restore purposes.
- app = mPackageManager.getApplicationInfo(pkg.packageName,
- PackageManager.GET_SHARED_LIBRARY_FILES);
- pkg.applicationInfo.sharedLibraryFiles = app.sharedLibraryFiles;
- }
- } catch (NameNotFoundException e) {
- packages.remove(a);
- }
- }
- return packages;
- }
-
- // Called from the backup tasks: record that the given app has been successfully
- // backed up at least once. This includes both key/value and full-data backups
- // through the transport.
- void logBackupComplete(String packageName) {
- if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) return;
-
- synchronized (mEverStoredApps) {
- if (!mEverStoredApps.add(packageName)) return;
-
- RandomAccessFile out = null;
- try {
- out = new RandomAccessFile(mEverStored, "rws");
- out.seek(out.length());
- out.writeUTF(packageName);
- } catch (IOException e) {
- Slog.e(TAG, "Can't log backup of " + packageName + " to " + mEverStored);
- } finally {
- try { if (out != null) out.close(); } catch (IOException e) {}
- }
- }
- }
-
- // Persistently record the current and ancestral backup tokens as well
- // as the set of packages with data [supposedly] available in the
- // ancestral dataset.
- void writeRestoreTokens() {
- try {
- RandomAccessFile af = new RandomAccessFile(mTokenFile, "rwd");
-
- // First, the version number of this record, for futureproofing
- af.writeInt(CURRENT_ANCESTRAL_RECORD_VERSION);
-
- // Write the ancestral and current tokens
- af.writeLong(mAncestralToken);
- af.writeLong(mCurrentToken);
-
- // Now write the set of ancestral packages
- if (mAncestralPackages == null) {
- af.writeInt(-1);
- } else {
- af.writeInt(mAncestralPackages.size());
- if (DEBUG) Slog.v(TAG, "Ancestral packages: " + mAncestralPackages.size());
- for (String pkgName : mAncestralPackages) {
- af.writeUTF(pkgName);
- if (MORE_DEBUG) Slog.v(TAG, " " + pkgName);
- }
- }
- af.close();
- } catch (IOException e) {
- Slog.w(TAG, "Unable to write token file:", e);
- }
- }
-
- // What name is this transport registered under...?
- private String getTransportName(IBackupTransport transport) {
- if (MORE_DEBUG) {
- Slog.v(TAG, "Searching for transport name of " + transport);
- }
- return mTransportManager.getTransportName(transport);
- }
-
- // fire off a backup agent, blocking until it attaches or times out
- @Override
- public IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) {
- IBackupAgent agent = null;
- synchronized(mAgentConnectLock) {
- mConnecting = true;
- mConnectedAgent = null;
- try {
- if (mActivityManager.bindBackupAgent(app.packageName, mode,
- UserHandle.USER_OWNER)) {
- Slog.d(TAG, "awaiting agent for " + app);
-
- // success; wait for the agent to arrive
- // only wait 10 seconds for the bind to happen
- long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL;
- while (mConnecting && mConnectedAgent == null
- && (System.currentTimeMillis() < timeoutMark)) {
- try {
- mAgentConnectLock.wait(5000);
- } catch (InterruptedException e) {
- // just bail
- Slog.w(TAG, "Interrupted: " + e);
- mConnecting = false;
- mConnectedAgent = null;
- }
- }
-
- // if we timed out with no connect, abort and move on
- if (mConnecting == true) {
- Slog.w(TAG, "Timeout waiting for agent " + app);
- mConnectedAgent = null;
- }
- if (DEBUG) Slog.i(TAG, "got agent " + mConnectedAgent);
- agent = mConnectedAgent;
- }
- } catch (RemoteException e) {
- // can't happen - ActivityManager is local
- }
- }
- if (agent == null) {
- try {
- mActivityManager.clearPendingBackup();
- } catch (RemoteException e) {
- // can't happen - ActivityManager is local
- }
- }
- return agent;
- }
-
- // clear an application's data, blocking until the operation completes or times out
- void clearApplicationDataSynchronous(String packageName) {
- // Don't wipe packages marked allowClearUserData=false
- try {
- PackageInfo info = mPackageManager.getPackageInfo(packageName, 0);
- if ((info.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA) == 0) {
- if (MORE_DEBUG) Slog.i(TAG, "allowClearUserData=false so not wiping "
- + packageName);
- return;
- }
- } catch (NameNotFoundException e) {
- Slog.w(TAG, "Tried to clear data for " + packageName + " but not found");
- return;
- }
-
- ClearDataObserver observer = new ClearDataObserver();
-
- synchronized(mClearDataLock) {
- mClearingData = true;
- try {
- mActivityManager.clearApplicationUserData(packageName, observer, 0);
- } catch (RemoteException e) {
- // can't happen because the activity manager is in this process
- }
-
- // only wait 10 seconds for the clear data to happen
- long timeoutMark = System.currentTimeMillis() + TIMEOUT_INTERVAL;
- while (mClearingData && (System.currentTimeMillis() < timeoutMark)) {
- try {
- mClearDataLock.wait(5000);
- } catch (InterruptedException e) {
- // won't happen, but still.
- mClearingData = false;
- }
- }
- }
- }
-
- class ClearDataObserver extends IPackageDataObserver.Stub {
- public void onRemoveCompleted(String packageName, boolean succeeded) {
- synchronized(mClearDataLock) {
- mClearingData = false;
- mClearDataLock.notifyAll();
- }
- }
- }
-
- // Get the restore-set token for the best-available restore set for this package:
- // the active set if possible, else the ancestral one. Returns zero if none available.
- @Override
- public long getAvailableRestoreToken(String packageName) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "getAvailableRestoreToken");
-
- long token = mAncestralToken;
- synchronized (mQueueLock) {
- if (mCurrentToken != 0 && mEverStoredApps.contains(packageName)) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "App in ever-stored, so using current token");
- }
- token = mCurrentToken;
- }
- }
- if (MORE_DEBUG) Slog.i(TAG, "getAvailableRestoreToken() == " + token);
- return token;
- }
-
- @Override
- public int requestBackup(String[] packages, IBackupObserver observer, int flags) {
- return requestBackup(packages, observer, null, flags);
- }
-
- @Override
- public int requestBackup(String[] packages, IBackupObserver observer,
- IBackupManagerMonitor monitor, int flags) {
- mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "requestBackup");
-
- if (packages == null || packages.length < 1) {
- Slog.e(TAG, "No packages named for backup request");
- sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
- monitor = monitorEvent(monitor, BackupManagerMonitor.LOG_EVENT_ID_NO_PACKAGES,
- null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
- throw new IllegalArgumentException("No packages are provided for backup");
- }
-
- if (!mEnabled || !mProvisioned) {
- Slog.i(TAG, "Backup requested but e=" + mEnabled + " p=" +mProvisioned);
- sendBackupFinished(observer, BackupManager.ERROR_BACKUP_NOT_ALLOWED);
- final int logTag = mProvisioned
- ? BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED
- : BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
- monitor = monitorEvent(monitor, logTag, null,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
- return BackupManager.ERROR_BACKUP_NOT_ALLOWED;
- }
-
- IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
- if (transport == null) {
- sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
- monitor = monitorEvent(monitor, BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL,
- null, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
- return BackupManager.ERROR_TRANSPORT_ABORTED;
- }
-
- ArrayList<String> fullBackupList = new ArrayList<>();
- ArrayList<String> kvBackupList = new ArrayList<>();
- for (String packageName : packages) {
- if (PACKAGE_MANAGER_SENTINEL.equals(packageName)) {
- kvBackupList.add(packageName);
- continue;
- }
- try {
- PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName,
- PackageManager.GET_SIGNATURES);
- if (!appIsEligibleForBackup(packageInfo.applicationInfo, mPackageManager)) {
- sendBackupOnPackageResult(observer, packageName,
- BackupManager.ERROR_BACKUP_NOT_ALLOWED);
- continue;
- }
- if (appGetsFullBackup(packageInfo)) {
- fullBackupList.add(packageInfo.packageName);
- } else {
- kvBackupList.add(packageInfo.packageName);
- }
- } catch (NameNotFoundException e) {
- sendBackupOnPackageResult(observer, packageName,
- BackupManager.ERROR_PACKAGE_NOT_FOUND);
- }
- }
- EventLog.writeEvent(EventLogTags.BACKUP_REQUESTED, packages.length, kvBackupList.size(),
- fullBackupList.size());
- if (MORE_DEBUG) {
- Slog.i(TAG, "Backup requested for " + packages.length + " packages, of them: " +
- fullBackupList.size() + " full backups, " + kvBackupList.size() + " k/v backups");
- }
-
- String dirName;
- try {
- dirName = transport.transportDirName();
- } catch (Exception e) {
- Slog.e(TAG, "Transport unavailable while attempting backup: " + e.getMessage());
- sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
- return BackupManager.ERROR_TRANSPORT_ABORTED;
- }
-
- boolean nonIncrementalBackup = (flags & BackupManager.FLAG_NON_INCREMENTAL_BACKUP) != 0;
-
- Message msg = mBackupHandler.obtainMessage(MSG_REQUEST_BACKUP);
- msg.obj = new BackupParams(transport, dirName, kvBackupList, fullBackupList, observer,
- monitor, true, nonIncrementalBackup);
- mBackupHandler.sendMessage(msg);
- return BackupManager.SUCCESS;
- }
-
- // Cancel all running backups.
- @Override
- public void cancelBackups(){
- mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "cancelBackups");
- if (MORE_DEBUG) {
- Slog.i(TAG, "cancelBackups() called.");
- }
- final long oldToken = Binder.clearCallingIdentity();
- try {
- List<Integer> operationsToCancel = new ArrayList<>();
- synchronized (mCurrentOpLock) {
- for (int i = 0; i < mCurrentOperations.size(); i++) {
- Operation op = mCurrentOperations.valueAt(i);
- int token = mCurrentOperations.keyAt(i);
- if (op.type == OP_TYPE_BACKUP) {
- operationsToCancel.add(token);
- }
- }
- }
- for (Integer token : operationsToCancel) {
- handleCancel(token, true /* cancelAll */);
- }
- // We don't want the backup jobs to kick in any time soon.
- // Reschedules them to run in the distant future.
- KeyValueBackupJob.schedule(mContext, BUSY_BACKOFF_MIN_MILLIS, mConstants);
- FullBackupJob.schedule(mContext, 2 * BUSY_BACKOFF_MIN_MILLIS, mConstants);
- } finally {
- Binder.restoreCallingIdentity(oldToken);
- }
- }
-
- @Override
- public void prepareOperationTimeout(int token, long interval, BackupRestoreTask callback,
- int operationType) {
- if (operationType != OP_TYPE_BACKUP_WAIT && operationType != OP_TYPE_RESTORE_WAIT) {
- Slog.wtf(TAG, "prepareOperationTimeout() doesn't support operation " +
- Integer.toHexString(token) + " of type " + operationType);
- return;
- }
- if (MORE_DEBUG) Slog.v(TAG, "starting timeout: token=" + Integer.toHexString(token)
- + " interval=" + interval + " callback=" + callback);
-
- synchronized (mCurrentOpLock) {
- mCurrentOperations.put(token, new Operation(OP_PENDING, callback, operationType));
- Message msg = mBackupHandler.obtainMessage(getMessageIdForOperationType(operationType),
- token, 0, callback);
- mBackupHandler.sendMessageDelayed(msg, interval);
- }
- }
-
- private int getMessageIdForOperationType(int operationType) {
- switch (operationType) {
- case OP_TYPE_BACKUP_WAIT:
- return MSG_BACKUP_OPERATION_TIMEOUT;
- case OP_TYPE_RESTORE_WAIT:
- return MSG_RESTORE_OPERATION_TIMEOUT;
- default:
- Slog.wtf(TAG, "getMessageIdForOperationType called on invalid operation type: " +
- operationType);
- return -1;
- }
- }
-
- private void removeOperation(int token) {
- if (MORE_DEBUG) {
- Slog.d(TAG, "Removing operation token=" + Integer.toHexString(token));
- }
- synchronized (mCurrentOpLock) {
- if (mCurrentOperations.get(token) == null) {
- Slog.w(TAG, "Duplicate remove for operation. token=" +
- Integer.toHexString(token));
- }
- mCurrentOperations.remove(token);
- }
- }
-
- // synchronous waiter case
- @Override
- public boolean waitUntilOperationComplete(int token) {
- if (MORE_DEBUG) Slog.i(TAG, "Blocking until operation complete for "
- + Integer.toHexString(token));
- int finalState = OP_PENDING;
- Operation op = null;
- synchronized (mCurrentOpLock) {
- while (true) {
- op = mCurrentOperations.get(token);
- if (op == null) {
- // mysterious disappearance: treat as success with no callback
- break;
- } else {
- if (op.state == OP_PENDING) {
- try {
- mCurrentOpLock.wait();
- } catch (InterruptedException e) {
- }
- // When the wait is notified we loop around and recheck the current state
- } else {
- if (MORE_DEBUG) {
- Slog.d(TAG, "Unblocked waiting for operation token=" +
- Integer.toHexString(token));
- }
- // No longer pending; we're done
- finalState = op.state;
- break;
- }
- }
- }
- }
-
- removeOperation(token);
- if (op != null) {
- mBackupHandler.removeMessages(getMessageIdForOperationType(op.type));
- }
- if (MORE_DEBUG) Slog.v(TAG, "operation " + Integer.toHexString(token)
- + " complete: finalState=" + finalState);
- return finalState == OP_ACKNOWLEDGED;
- }
-
- void handleCancel(int token, boolean cancelAll) {
- // Notify any synchronous waiters
- Operation op = null;
- synchronized (mCurrentOpLock) {
- op = mCurrentOperations.get(token);
- if (MORE_DEBUG) {
- if (op == null) Slog.w(TAG, "Cancel of token " + Integer.toHexString(token)
- + " but no op found");
- }
- int state = (op != null) ? op.state : OP_TIMEOUT;
- if (state == OP_ACKNOWLEDGED) {
- // The operation finished cleanly, so we have nothing more to do.
- if (DEBUG) {
- Slog.w(TAG, "Operation already got an ack." +
- "Should have been removed from mCurrentOperations.");
- }
- op = null;
- mCurrentOperations.delete(token);
- } else if (state == OP_PENDING) {
- if (DEBUG) Slog.v(TAG, "Cancel: token=" + Integer.toHexString(token));
- op.state = OP_TIMEOUT;
- // Can't delete op from mCurrentOperations here. waitUntilOperationComplete may be
- // called after we receive cancel here. We need this op's state there.
-
- // Remove all pending timeout messages of types OP_TYPE_BACKUP_WAIT and
- // OP_TYPE_RESTORE_WAIT. On the other hand, OP_TYPE_BACKUP cannot time out and
- // doesn't require cancellation.
- if (op.type == OP_TYPE_BACKUP_WAIT || op.type == OP_TYPE_RESTORE_WAIT) {
- mBackupHandler.removeMessages(getMessageIdForOperationType(op.type));
- }
- }
- mCurrentOpLock.notifyAll();
- }
-
- // If there's a TimeoutHandler for this event, call it
- if (op != null && op.callback != null) {
- if (MORE_DEBUG) {
- Slog.v(TAG, " Invoking cancel on " + op.callback);
- }
- op.callback.handleCancel(cancelAll);
- }
- }
-
- // ----- Back up a set of applications via a worker thread -----
-
- enum BackupState {
- INITIAL,
- RUNNING_QUEUE,
- FINAL
- }
-
- /**
- * This class handles the process of backing up a given list of key/value backup packages.
- * Also takes in a list of pending dolly backups and kicks them off when key/value backups
- * are done.
- *
- * Flow:
- * If required, backup @pm@.
- * For each pending key/value backup package:
- * - Bind to agent.
- * - Call agent.doBackup()
- * - Wait either for cancel/timeout or operationComplete() callback from the agent.
- * Start task to perform dolly backups.
- *
- * There are three entry points into this class:
- * - execute() [Called from the handler thread]
- * - operationComplete(long result) [Called from the handler thread]
- * - handleCancel(boolean cancelAll) [Can be called from any thread]
- * These methods synchronize on mCancelLock.
- *
- * Interaction with mCurrentOperations:
- * - An entry for this task is put into mCurrentOperations for the entire lifetime of the
- * task. This is useful to cancel the task if required.
- * - An ephemeral entry is put into mCurrentOperations each time we are waiting on for
- * response from a backup agent. This is used to plumb timeouts and completion callbacks.
- */
- class PerformBackupTask implements BackupRestoreTask {
- private static final String TAG = "PerformBackupTask";
-
- private final Object mCancelLock = new Object();
-
- IBackupTransport mTransport;
- ArrayList<BackupRequest> mQueue;
- ArrayList<BackupRequest> mOriginalQueue;
- File mStateDir;
- File mJournal;
- BackupState mCurrentState;
- List<String> mPendingFullBackups;
- IBackupObserver mObserver;
- IBackupManagerMonitor mMonitor;
-
- private final PerformFullTransportBackupTask mFullBackupTask;
- private final int mCurrentOpToken;
- private volatile int mEphemeralOpToken;
-
- // carried information about the current in-flight operation
- IBackupAgent mAgentBinder;
- PackageInfo mCurrentPackage;
- File mSavedStateName;
- File mBackupDataName;
- File mNewStateName;
- ParcelFileDescriptor mSavedState;
- ParcelFileDescriptor mBackupData;
- ParcelFileDescriptor mNewState;
- int mStatus;
- boolean mFinished;
- final boolean mUserInitiated;
- final boolean mNonIncremental;
-
- private volatile boolean mCancelAll;
-
- public PerformBackupTask(IBackupTransport transport, String dirName,
- ArrayList<BackupRequest> queue, File journal, IBackupObserver observer,
- IBackupManagerMonitor monitor, List<String> pendingFullBackups,
- boolean userInitiated, boolean nonIncremental) {
- mTransport = transport;
- mOriginalQueue = queue;
- mQueue = new ArrayList<>();
- mJournal = journal;
- mObserver = observer;
- mMonitor = monitor;
- mPendingFullBackups = pendingFullBackups;
- mUserInitiated = userInitiated;
- mNonIncremental = nonIncremental;
-
- mStateDir = new File(mBaseStateDir, dirName);
- mCurrentOpToken = generateRandomIntegerToken();
-
- mFinished = false;
-
- synchronized (mCurrentOpLock) {
- if (isBackupOperationInProgress()) {
- if (DEBUG) {
- Slog.d(TAG, "Skipping backup since one is already in progress.");
- }
- mCancelAll = true;
- mFullBackupTask = null;
- mCurrentState = BackupState.FINAL;
- addBackupTrace("Skipped. Backup already in progress.");
- } else {
- mCurrentState = BackupState.INITIAL;
- CountDownLatch latch = new CountDownLatch(1);
- String[] fullBackups =
- mPendingFullBackups.toArray(new String[mPendingFullBackups.size()]);
- mFullBackupTask =
- new PerformFullTransportBackupTask(/*fullBackupRestoreObserver*/ null,
- fullBackups, /*updateSchedule*/ false, /*runningJob*/ null,
- latch,
- mObserver, mMonitor, mUserInitiated);
-
- registerTask();
- addBackupTrace("STATE => INITIAL");
- }
- }
- }
-
- /**
- * Put this task in the repository of running tasks.
- */
- private void registerTask() {
- synchronized (mCurrentOpLock) {
- mCurrentOperations.put(mCurrentOpToken, new Operation(OP_PENDING, this,
- OP_TYPE_BACKUP));
- }
- }
-
- /**
- * Remove this task from repository of running tasks.
- */
- private void unregisterTask() {
- removeOperation(mCurrentOpToken);
- }
-
- // Main entry point: perform one chunk of work, updating the state as appropriate
- // and reposting the next chunk to the primary backup handler thread.
- @Override
- @GuardedBy("mCancelLock")
- public void execute() {
- synchronized (mCancelLock) {
- switch (mCurrentState) {
- case INITIAL:
- beginBackup();
- break;
-
- case RUNNING_QUEUE:
- invokeNextAgent();
- break;
-
- case FINAL:
- if (!mFinished) {
- finalizeBackup();
- } else {
- Slog.e(TAG, "Duplicate finish of K/V pass");
- }
- break;
- }
- }
- }
-
- // We're starting a backup pass. Initialize the transport and send
- // the PM metadata blob if we haven't already.
- void beginBackup() {
- if (DEBUG_BACKUP_TRACE) {
- clearBackupTrace();
- StringBuilder b = new StringBuilder(256);
- b.append("beginBackup: [");
- for (BackupRequest req : mOriginalQueue) {
- b.append(' ');
- b.append(req.packageName);
- }
- b.append(" ]");
- addBackupTrace(b.toString());
- }
-
- mAgentBinder = null;
- mStatus = BackupTransport.TRANSPORT_OK;
-
- // Sanity check: if the queue is empty we have no work to do.
- if (mOriginalQueue.isEmpty() && mPendingFullBackups.isEmpty()) {
- Slog.w(TAG, "Backup begun with an empty queue - nothing to do.");
- addBackupTrace("queue empty at begin");
- sendBackupFinished(mObserver, BackupManager.SUCCESS);
- executeNextState(BackupState.FINAL);
- return;
- }
-
- // We need to retain the original queue contents in case of transport
- // failure, but we want a working copy that we can manipulate along
- // the way.
- mQueue = (ArrayList<BackupRequest>) mOriginalQueue.clone();
-
- // When the transport is forcing non-incremental key/value payloads, we send the
- // metadata only if it explicitly asks for it.
- boolean skipPm = mNonIncremental;
-
- // The app metadata pseudopackage might also be represented in the
- // backup queue if apps have been added/removed since the last time
- // we performed a backup. Drop it from the working queue now that
- // we're committed to evaluating it for backup regardless.
- for (int i = 0; i < mQueue.size(); i++) {
- if (PACKAGE_MANAGER_SENTINEL.equals(mQueue.get(i).packageName)) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Metadata in queue; eliding");
- }
- mQueue.remove(i);
- skipPm = false;
- break;
- }
- }
-
- if (DEBUG) Slog.v(TAG, "Beginning backup of " + mQueue.size() + " targets");
-
- File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL);
- try {
- final String transportName = mTransport.transportDirName();
- EventLog.writeEvent(EventLogTags.BACKUP_START, transportName);
-
- // If we haven't stored package manager metadata yet, we must init the transport.
- if (mStatus == BackupTransport.TRANSPORT_OK && pmState.length() <= 0) {
- Slog.i(TAG, "Initializing (wiping) backup state and transport storage");
- addBackupTrace("initializing transport " + transportName);
- resetBackupState(mStateDir); // Just to make sure.
- mStatus = mTransport.initializeDevice();
-
- addBackupTrace("transport.initializeDevice() == " + mStatus);
- if (mStatus == BackupTransport.TRANSPORT_OK) {
- EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE);
- } else {
- EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)");
- Slog.e(TAG, "Transport error in initializeDevice()");
- }
- }
-
- if (skipPm) {
- Slog.d(TAG, "Skipping backup of package metadata.");
- executeNextState(BackupState.RUNNING_QUEUE);
- } else {
- // The package manager doesn't have a proper <application> etc, but since
- // it's running here in the system process we can just set up its agent
- // directly and use a synthetic BackupRequest. We always run this pass
- // because it's cheap and this way we guarantee that we don't get out of
- // step even if we're selecting among various transports at run time.
- if (mStatus == BackupTransport.TRANSPORT_OK) {
- PackageManagerBackupAgent pmAgent = makeMetadataAgent();
- mStatus = invokeAgentForBackup(PACKAGE_MANAGER_SENTINEL,
- IBackupAgent.Stub.asInterface(pmAgent.onBind()), mTransport);
- addBackupTrace("PMBA invoke: " + mStatus);
-
- // Because the PMBA is a local instance, it has already executed its
- // backup callback and returned. Blow away the lingering (spurious)
- // pending timeout message for it.
- mBackupHandler.removeMessages(MSG_BACKUP_OPERATION_TIMEOUT);
- }
- }
-
- if (mStatus == BackupTransport.TRANSPORT_NOT_INITIALIZED) {
- // The backend reports that our dataset has been wiped. Note this in
- // the event log; the no-success code below will reset the backup
- // state as well.
- EventLog.writeEvent(EventLogTags.BACKUP_RESET, transportName);
- }
- } catch (Exception e) {
- Slog.e(TAG, "Error in backup thread", e);
- addBackupTrace("Exception in backup thread: " + e);
- mStatus = BackupTransport.TRANSPORT_ERROR;
- } finally {
- // If we've succeeded so far, invokeAgentForBackup() will have run the PM
- // metadata and its completion/timeout callback will continue the state
- // machine chain. If it failed that won't happen; we handle that now.
- addBackupTrace("exiting prelim: " + mStatus);
- if (mStatus != BackupTransport.TRANSPORT_OK) {
- // if things went wrong at this point, we need to
- // restage everything and try again later.
- resetBackupState(mStateDir); // Just to make sure.
- // In case of any other error, it's backup transport error.
- sendBackupFinished(mObserver, BackupManager.ERROR_TRANSPORT_ABORTED);
- executeNextState(BackupState.FINAL);
- }
- }
- }
-
- // Transport has been initialized and the PM metadata submitted successfully
- // if that was warranted. Now we process the single next thing in the queue.
- void invokeNextAgent() {
- mStatus = BackupTransport.TRANSPORT_OK;
- addBackupTrace("invoke q=" + mQueue.size());
-
- // Sanity check that we have work to do. If not, skip to the end where
- // we reestablish the wakelock invariants etc.
- if (mQueue.isEmpty()) {
- if (MORE_DEBUG) Slog.i(TAG, "queue now empty");
- executeNextState(BackupState.FINAL);
- return;
- }
-
- // pop the entry we're going to process on this step
- BackupRequest request = mQueue.get(0);
- mQueue.remove(0);
-
- Slog.d(TAG, "starting key/value backup of " + request);
- addBackupTrace("launch agent for " + request.packageName);
-
- // Verify that the requested app exists; it might be something that
- // requested a backup but was then uninstalled. The request was
- // journalled and rather than tamper with the journal it's safer
- // to sanity-check here. This also gives us the classname of the
- // package's backup agent.
- try {
- mCurrentPackage = mPackageManager.getPackageInfo(request.packageName,
- PackageManager.GET_SIGNATURES);
- if (!appIsEligibleForBackup(mCurrentPackage.applicationInfo, mPackageManager)) {
- // The manifest has changed but we had a stale backup request pending.
- // This won't happen again because the app won't be requesting further
- // backups.
- Slog.i(TAG, "Package " + request.packageName
- + " no longer supports backup; skipping");
- addBackupTrace("skipping - not eligible, completion is noop");
- // Shouldn't happen in case of requested backup, as pre-check was done in
- // #requestBackup(), except to app update done concurrently
- sendBackupOnPackageResult(mObserver, mCurrentPackage.packageName,
- BackupManager.ERROR_BACKUP_NOT_ALLOWED);
- executeNextState(BackupState.RUNNING_QUEUE);
- return;
- }
-
- if (appGetsFullBackup(mCurrentPackage)) {
- // It's possible that this app *formerly* was enqueued for key/value backup,
- // but has since been updated and now only supports the full-data path.
- // Don't proceed with a key/value backup for it in this case.
- Slog.i(TAG, "Package " + request.packageName
- + " requests full-data rather than key/value; skipping");
- addBackupTrace("skipping - fullBackupOnly, completion is noop");
- // Shouldn't happen in case of requested backup, as pre-check was done in
- // #requestBackup()
- sendBackupOnPackageResult(mObserver, mCurrentPackage.packageName,
- BackupManager.ERROR_BACKUP_NOT_ALLOWED);
- executeNextState(BackupState.RUNNING_QUEUE);
- return;
- }
-
- if (appIsStopped(mCurrentPackage.applicationInfo)) {
- // The app has been force-stopped or cleared or just installed,
- // and not yet launched out of that state, so just as it won't
- // receive broadcasts, we won't run it for backup.
- addBackupTrace("skipping - stopped");
- sendBackupOnPackageResult(mObserver, mCurrentPackage.packageName,
- BackupManager.ERROR_BACKUP_NOT_ALLOWED);
- executeNextState(BackupState.RUNNING_QUEUE);
- return;
- }
-
- IBackupAgent agent = null;
- try {
- mWakelock.setWorkSource(new WorkSource(mCurrentPackage.applicationInfo.uid));
- agent = bindToAgentSynchronous(mCurrentPackage.applicationInfo,
- ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL);
- addBackupTrace("agent bound; a? = " + (agent != null));
- if (agent != null) {
- mAgentBinder = agent;
- mStatus = invokeAgentForBackup(request.packageName, agent, mTransport);
- // at this point we'll either get a completion callback from the
- // agent, or a timeout message on the main handler. either way, we're
- // done here as long as we're successful so far.
- } else {
- // Timeout waiting for the agent
- mStatus = BackupTransport.AGENT_ERROR;
- }
- } catch (SecurityException ex) {
- // Try for the next one.
- Slog.d(TAG, "error in bind/backup", ex);
- mStatus = BackupTransport.AGENT_ERROR;
- addBackupTrace("agent SE");
- }
- } catch (NameNotFoundException e) {
- Slog.d(TAG, "Package does not exist; skipping");
- addBackupTrace("no such package");
- mStatus = BackupTransport.AGENT_UNKNOWN;
- } finally {
- mWakelock.setWorkSource(null);
-
- // If there was an agent error, no timeout/completion handling will occur.
- // That means we need to direct to the next state ourselves.
- if (mStatus != BackupTransport.TRANSPORT_OK) {
- BackupState nextState = BackupState.RUNNING_QUEUE;
- mAgentBinder = null;
-
- // An agent-level failure means we reenqueue this one agent for
- // a later retry, but otherwise proceed normally.
- if (mStatus == BackupTransport.AGENT_ERROR) {
- if (MORE_DEBUG) Slog.i(TAG, "Agent failure for " + request.packageName
- + " - restaging");
- dataChangedImpl(request.packageName);
- mStatus = BackupTransport.TRANSPORT_OK;
- if (mQueue.isEmpty()) nextState = BackupState.FINAL;
- sendBackupOnPackageResult(mObserver, mCurrentPackage.packageName,
- BackupManager.ERROR_AGENT_FAILURE);
- } else if (mStatus == BackupTransport.AGENT_UNKNOWN) {
- // Failed lookup of the app, so we couldn't bring up an agent, but
- // we're otherwise fine. Just drop it and go on to the next as usual.
- mStatus = BackupTransport.TRANSPORT_OK;
- sendBackupOnPackageResult(mObserver, mCurrentPackage.packageName,
- BackupManager.ERROR_PACKAGE_NOT_FOUND);
- } else {
- // Transport-level failure means we reenqueue everything
- revertAndEndBackup();
- nextState = BackupState.FINAL;
- }
-
- executeNextState(nextState);
- } else {
- // success case
- addBackupTrace("expecting completion/timeout callback");
- }
- }
- }
-
- void finalizeBackup() {
- addBackupTrace("finishing");
-
- // Mark packages that we didn't backup (because backup was cancelled, etc.) as needing
- // backup.
- for (BackupRequest req : mQueue) {
- dataChangedImpl(req.packageName);
- }
-
- // Either backup was successful, in which case we of course do not need
- // this pass's journal any more; or it failed, in which case we just
- // re-enqueued all of these packages in the current active journal.
- // Either way, we no longer need this pass's journal.
- if (mJournal != null && !mJournal.delete()) {
- Slog.e(TAG, "Unable to remove backup journal file " + mJournal);
- }
-
- // If everything actually went through and this is the first time we've
- // done a backup, we can now record what the current backup dataset token
- // is.
- if ((mCurrentToken == 0) && (mStatus == BackupTransport.TRANSPORT_OK)) {
- addBackupTrace("success; recording token");
- try {
- mCurrentToken = mTransport.getCurrentRestoreSet();
- writeRestoreTokens();
- } catch (Exception e) {
- // nothing for it at this point, unfortunately, but this will be
- // recorded the next time we fully succeed.
- Slog.e(TAG, "Transport threw reporting restore set: " + e.getMessage());
- addBackupTrace("transport threw returning token");
- }
- }
-
- // Set up the next backup pass - at this point we can set mBackupRunning
- // to false to allow another pass to fire, because we're done with the
- // state machine sequence and the wakelock is refcounted.
- synchronized (mQueueLock) {
- mBackupRunning = false;
- if (mStatus == BackupTransport.TRANSPORT_NOT_INITIALIZED) {
- // Make sure we back up everything and perform the one-time init
- if (MORE_DEBUG) Slog.d(TAG, "Server requires init; rerunning");
- addBackupTrace("init required; rerunning");
- try {
- final String name = mTransportManager.getTransportName(mTransport);
- if (name != null) {
- mPendingInits.add(name);
- } else {
- if (DEBUG) {
- Slog.w(TAG, "Couldn't find name of transport " + mTransport
- + " for init");
- }
- }
- } catch (Exception e) {
- Slog.w(TAG, "Failed to query transport name for init: " + e.getMessage());
- // swallow it and proceed; we don't rely on this
- }
- clearMetadata();
- backupNow();
- }
- }
-
- clearBackupTrace();
-
- unregisterTask();
-
- if (!mCancelAll && mStatus == BackupTransport.TRANSPORT_OK &&
- mPendingFullBackups != null && !mPendingFullBackups.isEmpty()) {
- Slog.d(TAG, "Starting full backups for: " + mPendingFullBackups);
- // Acquiring wakelock for PerformFullTransportBackupTask before its start.
- mWakelock.acquire();
- (new Thread(mFullBackupTask, "full-transport-requested")).start();
- } else if (mCancelAll) {
- if (mFullBackupTask != null) {
- mFullBackupTask.unregisterTask();
- }
- sendBackupFinished(mObserver, BackupManager.ERROR_BACKUP_CANCELLED);
- } else {
- mFullBackupTask.unregisterTask();
- switch (mStatus) {
- case BackupTransport.TRANSPORT_OK:
- sendBackupFinished(mObserver, BackupManager.SUCCESS);
- break;
- case BackupTransport.TRANSPORT_NOT_INITIALIZED:
- sendBackupFinished(mObserver, BackupManager.ERROR_TRANSPORT_ABORTED);
- break;
- case BackupTransport.TRANSPORT_ERROR:
- default:
- sendBackupFinished(mObserver, BackupManager.ERROR_TRANSPORT_ABORTED);
- break;
- }
- }
- mFinished = true;
- Slog.i(BackupManagerService.TAG, "K/V backup pass finished.");
- // Only once we're entirely finished do we release the wakelock for k/v backup.
- mWakelock.release();
- }
-
- // Remove the PM metadata state. This will generate an init on the next pass.
- void clearMetadata() {
- final File pmState = new File(mStateDir, PACKAGE_MANAGER_SENTINEL);
- if (pmState.exists()) pmState.delete();
- }
-
- // Invoke an agent's doBackup() and start a timeout message spinning on the main
- // handler in case it doesn't get back to us.
- int invokeAgentForBackup(String packageName, IBackupAgent agent,
- IBackupTransport transport) {
- if (DEBUG) Slog.d(TAG, "invokeAgentForBackup on " + packageName);
- addBackupTrace("invoking " + packageName);
-
- File blankStateName = new File(mStateDir, "blank_state");
- mSavedStateName = new File(mStateDir, packageName);
- mBackupDataName = new File(mDataDir, packageName + ".data");
- mNewStateName = new File(mStateDir, packageName + ".new");
- if (MORE_DEBUG) Slog.d(TAG, "data file: " + mBackupDataName);
-
- mSavedState = null;
- mBackupData = null;
- mNewState = null;
-
- boolean callingAgent = false;
- mEphemeralOpToken = generateRandomIntegerToken();
- try {
- // Look up the package info & signatures. This is first so that if it
- // throws an exception, there's no file setup yet that would need to
- // be unraveled.
- if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) {
- // The metadata 'package' is synthetic; construct one and make
- // sure our global state is pointed at it
- mCurrentPackage = new PackageInfo();
- mCurrentPackage.packageName = packageName;
- }
-
- // In a full backup, we pass a null ParcelFileDescriptor as
- // the saved-state "file". For key/value backups we pass the old state if
- // an incremental backup is required, and a blank state otherwise.
- mSavedState = ParcelFileDescriptor.open(
- mNonIncremental ? blankStateName : mSavedStateName,
- ParcelFileDescriptor.MODE_READ_ONLY |
- ParcelFileDescriptor.MODE_CREATE); // Make an empty file if necessary
-
- mBackupData = ParcelFileDescriptor.open(mBackupDataName,
- ParcelFileDescriptor.MODE_READ_WRITE |
- ParcelFileDescriptor.MODE_CREATE |
- ParcelFileDescriptor.MODE_TRUNCATE);
-
- if (!SELinux.restorecon(mBackupDataName)) {
- Slog.e(TAG, "SELinux restorecon failed on " + mBackupDataName);
- }
-
- mNewState = ParcelFileDescriptor.open(mNewStateName,
- ParcelFileDescriptor.MODE_READ_WRITE |
- ParcelFileDescriptor.MODE_CREATE |
- ParcelFileDescriptor.MODE_TRUNCATE);
-
- final long quota = mTransport.getBackupQuota(packageName, false /* isFullBackup */);
- callingAgent = true;
-
- // Initiate the target's backup pass
- addBackupTrace("setting timeout");
- prepareOperationTimeout(mEphemeralOpToken, TIMEOUT_BACKUP_INTERVAL, this,
- OP_TYPE_BACKUP_WAIT);
- addBackupTrace("calling agent doBackup()");
-
- agent.doBackup(mSavedState, mBackupData, mNewState, quota, mEphemeralOpToken,
- mBackupManagerBinder);
- } catch (Exception e) {
- Slog.e(TAG, "Error invoking for backup on " + packageName + ". " + e);
- addBackupTrace("exception: " + e);
- EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName,
- e.toString());
- errorCleanup();
- return callingAgent ? BackupTransport.AGENT_ERROR
- : BackupTransport.TRANSPORT_ERROR;
- } finally {
- if (mNonIncremental) {
- blankStateName.delete();
- }
- }
-
- // At this point the agent is off and running. The next thing to happen will
- // either be a callback from the agent, at which point we'll process its data
- // for transport, or a timeout. Either way the next phase will happen in
- // response to the TimeoutHandler interface callbacks.
- addBackupTrace("invoke success");
- return BackupTransport.TRANSPORT_OK;
- }
-
- public void failAgent(IBackupAgent agent, String message) {
- try {
- agent.fail(message);
- } catch (Exception e) {
- Slog.w(TAG, "Error conveying failure to " + mCurrentPackage.packageName);
- }
- }
-
- // SHA-1 a byte array and return the result in hex
- private String SHA1Checksum(byte[] input) {
- final byte[] checksum;
- try {
- MessageDigest md = MessageDigest.getInstance("SHA-1");
- checksum = md.digest(input);
- } catch (NoSuchAlgorithmException e) {
- Slog.e(TAG, "Unable to use SHA-1!");
- return "00";
- }
-
- StringBuffer sb = new StringBuffer(checksum.length * 2);
- for (int i = 0; i < checksum.length; i++) {
- sb.append(Integer.toHexString(checksum[i]));
- }
- return sb.toString();
- }
-
- private void writeWidgetPayloadIfAppropriate(FileDescriptor fd, String pkgName)
- throws IOException {
- // TODO: http://b/22388012
- byte[] widgetState = AppWidgetBackupBridge.getWidgetState(pkgName,
- UserHandle.USER_SYSTEM);
- // has the widget state changed since last time?
- final File widgetFile = new File(mStateDir, pkgName + "_widget");
- final boolean priorStateExists = widgetFile.exists();
-
- if (MORE_DEBUG) {
- if (priorStateExists || widgetState != null) {
- Slog.i(TAG, "Checking widget update: state=" + (widgetState != null)
- + " prior=" + priorStateExists);
- }
- }
-
- if (!priorStateExists && widgetState == null) {
- // no prior state, no new state => nothing to do
- return;
- }
-
- // if the new state is not null, we might need to compare checksums to
- // determine whether to update the widget blob in the archive. If the
- // widget state *is* null, we know a priori at this point that we simply
- // need to commit a deletion for it.
- String newChecksum = null;
- if (widgetState != null) {
- newChecksum = SHA1Checksum(widgetState);
- if (priorStateExists) {
- final String priorChecksum;
- try (
- FileInputStream fin = new FileInputStream(widgetFile);
- DataInputStream in = new DataInputStream(fin)
- ) {
- priorChecksum = in.readUTF();
- }
- if (Objects.equals(newChecksum, priorChecksum)) {
- // Same checksum => no state change => don't rewrite the widget data
- return;
- }
- }
- } // else widget state *became* empty, so we need to commit a deletion
-
- BackupDataOutput out = new BackupDataOutput(fd);
- if (widgetState != null) {
- try (
- FileOutputStream fout = new FileOutputStream(widgetFile);
- DataOutputStream stateOut = new DataOutputStream(fout)
- ) {
- stateOut.writeUTF(newChecksum);
- }
-
- out.writeEntityHeader(KEY_WIDGET_STATE, widgetState.length);
- out.writeEntityData(widgetState, widgetState.length);
- } else {
- // Widget state for this app has been removed; commit a deletion
- out.writeEntityHeader(KEY_WIDGET_STATE, -1);
- widgetFile.delete();
- }
- }
-
- @Override
- @GuardedBy("mCancelLock")
- public void operationComplete(long unusedResult) {
- removeOperation(mEphemeralOpToken);
- synchronized (mCancelLock) {
- // The agent reported back to us!
- if (mFinished) {
- Slog.d(TAG, "operationComplete received after task finished.");
- return;
- }
-
- if (mBackupData == null) {
- // This callback was racing with our timeout, so we've cleaned up the
- // agent state already and are on to the next thing. We have nothing
- // further to do here: agent state having been cleared means that we've
- // initiated the appropriate next operation.
- final String pkg = (mCurrentPackage != null)
- ? mCurrentPackage.packageName : "[none]";
- if (MORE_DEBUG) {
- Slog.i(TAG, "Callback after agent teardown: " + pkg);
- }
- addBackupTrace("late opComplete; curPkg = " + pkg);
- return;
- }
-
- final String pkgName = mCurrentPackage.packageName;
- final long filepos = mBackupDataName.length();
- FileDescriptor fd = mBackupData.getFileDescriptor();
- try {
- // If it's a 3rd party app, see whether they wrote any protected keys
- // and complain mightily if they are attempting shenanigans.
- if (mCurrentPackage.applicationInfo != null &&
- (mCurrentPackage.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
- == 0) {
- ParcelFileDescriptor readFd = ParcelFileDescriptor.open(mBackupDataName,
- ParcelFileDescriptor.MODE_READ_ONLY);
- BackupDataInput in = new BackupDataInput(readFd.getFileDescriptor());
- try {
- while (in.readNextHeader()) {
- final String key = in.getKey();
- if (key != null && key.charAt(0) >= 0xff00) {
- // Not okay: crash them and bail.
- failAgent(mAgentBinder, "Illegal backup key: " + key);
- addBackupTrace("illegal key " + key + " from " + pkgName);
- EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, pkgName,
- "bad key");
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_ILLEGAL_KEY,
- mCurrentPackage,
- BackupManagerMonitor
- .LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- putMonitoringExtra(null,
- BackupManagerMonitor.EXTRA_LOG_ILLEGAL_KEY,
- key));
- mBackupHandler.removeMessages(MSG_BACKUP_OPERATION_TIMEOUT);
- sendBackupOnPackageResult(mObserver, pkgName,
- BackupManager.ERROR_AGENT_FAILURE);
- errorCleanup();
- // agentErrorCleanup() implicitly executes next state properly
- return;
- }
- in.skipEntityData();
- }
- } finally {
- if (readFd != null) {
- readFd.close();
- }
- }
- }
-
- // Piggyback the widget state payload, if any
- writeWidgetPayloadIfAppropriate(fd, pkgName);
- } catch (IOException e) {
- // Hard disk error; recovery/failure policy TBD. For now roll back,
- // but we may want to consider this a transport-level failure (i.e.
- // we're in such a bad state that we can't contemplate doing backup
- // operations any more during this pass).
- Slog.w(TAG, "Unable to save widget state for " + pkgName);
- try {
- Os.ftruncate(fd, filepos);
- } catch (ErrnoException ee) {
- Slog.w(TAG, "Unable to roll back!");
- }
- }
-
- // Spin the data off to the transport and proceed with the next stage.
- if (MORE_DEBUG) Slog.v(TAG, "operationComplete(): sending data to transport for "
- + pkgName);
- mBackupHandler.removeMessages(MSG_BACKUP_OPERATION_TIMEOUT);
- clearAgentState();
- addBackupTrace("operation complete");
-
- ParcelFileDescriptor backupData = null;
- mStatus = BackupTransport.TRANSPORT_OK;
- long size = 0;
- try {
- size = mBackupDataName.length();
- if (size > 0) {
- if (mStatus == BackupTransport.TRANSPORT_OK) {
- backupData = ParcelFileDescriptor.open(mBackupDataName,
- ParcelFileDescriptor.MODE_READ_ONLY);
- addBackupTrace("sending data to transport");
- int flags = mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
- mStatus = mTransport.performBackup(mCurrentPackage, backupData, flags);
- }
-
- // TODO - We call finishBackup() for each application backed up, because
- // we need to know now whether it succeeded or failed. Instead, we should
- // hold off on finishBackup() until the end, which implies holding off on
- // renaming *all* the output state files (see below) until that happens.
-
- addBackupTrace("data delivered: " + mStatus);
- if (mStatus == BackupTransport.TRANSPORT_OK) {
- addBackupTrace("finishing op on transport");
- mStatus = mTransport.finishBackup();
- addBackupTrace("finished: " + mStatus);
- } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
- addBackupTrace("transport rejected package");
- }
- } else {
- if (MORE_DEBUG) Slog.i(TAG,
- "no backup data written; not calling transport");
- addBackupTrace("no data to send");
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- }
-
- if (mStatus == BackupTransport.TRANSPORT_OK) {
- // After successful transport, delete the now-stale data
- // and juggle the files so that next time we supply the agent
- // with the new state file it just created.
- mBackupDataName.delete();
- mNewStateName.renameTo(mSavedStateName);
- sendBackupOnPackageResult(mObserver, pkgName, BackupManager.SUCCESS);
- EventLog.writeEvent(EventLogTags.BACKUP_PACKAGE, pkgName, size);
- logBackupComplete(pkgName);
- } else if (mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
- // The transport has rejected backup of this specific package. Roll it
- // back but proceed with running the rest of the queue.
- mBackupDataName.delete();
- mNewStateName.delete();
- sendBackupOnPackageResult(mObserver, pkgName,
- BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED);
- EventLogTags.writeBackupAgentFailure(pkgName, "Transport rejected");
- } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
- sendBackupOnPackageResult(mObserver, pkgName,
- BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
- EventLog.writeEvent(EventLogTags.BACKUP_QUOTA_EXCEEDED, pkgName);
- } else {
- // Actual transport-level failure to communicate the data to the backend
- sendBackupOnPackageResult(mObserver, pkgName,
- BackupManager.ERROR_TRANSPORT_ABORTED);
- EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, pkgName);
- }
- } catch (Exception e) {
- sendBackupOnPackageResult(mObserver, pkgName,
- BackupManager.ERROR_TRANSPORT_ABORTED);
- Slog.e(TAG, "Transport error backing up " + pkgName, e);
- EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, pkgName);
- mStatus = BackupTransport.TRANSPORT_ERROR;
- } finally {
- try {
- if (backupData != null) backupData.close();
- } catch (IOException e) {
- }
- }
-
- final BackupState nextState;
- if (mStatus == BackupTransport.TRANSPORT_OK
- || mStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
- // Success or single-package rejection. Proceed with the next app if any,
- // otherwise we're done.
- nextState = (mQueue.isEmpty()) ? BackupState.FINAL : BackupState.RUNNING_QUEUE;
- } else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
- if (MORE_DEBUG) {
- Slog.d(TAG, "Package " + mCurrentPackage.packageName +
- " hit quota limit on k/v backup");
- }
- if (mAgentBinder != null) {
- try {
- long quota = mTransport.getBackupQuota(mCurrentPackage.packageName,
- false);
- mAgentBinder.doQuotaExceeded(size, quota);
- } catch (Exception e) {
- Slog.e(TAG, "Unable to notify about quota exceeded: " + e.getMessage());
- }
- }
- nextState = (mQueue.isEmpty()) ? BackupState.FINAL : BackupState.RUNNING_QUEUE;
- } else {
- // Any other error here indicates a transport-level failure. That means
- // we need to halt everything and reschedule everything for next time.
- revertAndEndBackup();
- nextState = BackupState.FINAL;
- }
-
- executeNextState(nextState);
- }
- }
-
-
- @Override
- @GuardedBy("mCancelLock")
- public void handleCancel(boolean cancelAll) {
- removeOperation(mEphemeralOpToken);
- synchronized (mCancelLock) {
- if (mFinished) {
- // We have already cancelled this operation.
- if (MORE_DEBUG) {
- Slog.d(TAG, "Ignoring stale cancel. cancelAll=" + cancelAll);
- }
- return;
- }
- mCancelAll = cancelAll;
- final String logPackageName = (mCurrentPackage != null)
- ? mCurrentPackage.packageName
- : "no_package_yet";
- Slog.i(TAG, "Cancel backing up " + logPackageName);
- EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, logPackageName);
- addBackupTrace("cancel of " + logPackageName + ", cancelAll=" + cancelAll);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_BACKUP_CANCEL,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
- putMonitoringExtra(null, BackupManagerMonitor.EXTRA_LOG_CANCEL_ALL,
- mCancelAll));
- errorCleanup();
- if (!cancelAll) {
- // The current agent either timed out or was cancelled running doBackup().
- // Restage it for the next time we run a backup pass.
- // !!! TODO: keep track of failure counts per agent, and blacklist those which
- // fail repeatedly (i.e. have proved themselves to be buggy).
- executeNextState(
- mQueue.isEmpty() ? BackupState.FINAL : BackupState.RUNNING_QUEUE);
- dataChangedImpl(mCurrentPackage.packageName);
- } else {
- finalizeBackup();
- }
- }
- }
-
- void revertAndEndBackup() {
- if (MORE_DEBUG) Slog.i(TAG, "Reverting backup queue - restaging everything");
- addBackupTrace("transport error; reverting");
-
- // We want to reset the backup schedule based on whatever the transport suggests
- // by way of retry/backoff time.
- long delay;
- try {
- delay = mTransport.requestBackupTime();
- } catch (Exception e) {
- Slog.w(TAG, "Unable to contact transport for recommended backoff: " + e.getMessage());
- delay = 0; // use the scheduler's default
- }
- KeyValueBackupJob.schedule(mContext, delay, mConstants);
-
- for (BackupRequest request : mOriginalQueue) {
- dataChangedImpl(request.packageName);
- }
-
- }
-
- void errorCleanup() {
- mBackupDataName.delete();
- mNewStateName.delete();
- clearAgentState();
- }
-
- // Cleanup common to both success and failure cases
- void clearAgentState() {
- try { if (mSavedState != null) mSavedState.close(); } catch (IOException e) {}
- try { if (mBackupData != null) mBackupData.close(); } catch (IOException e) {}
- try { if (mNewState != null) mNewState.close(); } catch (IOException e) {}
- synchronized (mCurrentOpLock) {
- // Current-operation callback handling requires the validity of these various
- // bits of internal state as an invariant of the operation still being live.
- // This means we make sure to clear all of the state in unison inside the lock.
- mCurrentOperations.remove(mEphemeralOpToken);
- mSavedState = mBackupData = mNewState = null;
- }
-
- // If this was a pseudopackage there's no associated Activity Manager state
- if (mCurrentPackage.applicationInfo != null) {
- addBackupTrace("unbinding " + mCurrentPackage.packageName);
- try { // unbind even on timeout, just in case
- mActivityManager.unbindBackupAgent(mCurrentPackage.applicationInfo);
- } catch (RemoteException e) { /* can't happen; activity manager is local */ }
- }
- }
-
- void executeNextState(BackupState nextState) {
- if (MORE_DEBUG) Slog.i(TAG, " => executing next step on "
- + this + " nextState=" + nextState);
- addBackupTrace("executeNextState => " + nextState);
- mCurrentState = nextState;
- Message msg = mBackupHandler.obtainMessage(MSG_BACKUP_RESTORE_STEP, this);
- mBackupHandler.sendMessage(msg);
- }
- }
-
- private boolean isBackupOperationInProgress() {
- synchronized (mCurrentOpLock) {
- for (int i = 0; i < mCurrentOperations.size(); i++) {
- Operation op = mCurrentOperations.valueAt(i);
- if (op.type == OP_TYPE_BACKUP && op.state == OP_PENDING) {
- return true;
- }
- }
- }
- return false;
- }
-
-
- // ----- Full backup/restore to a file/socket -----
-
- class FullBackupObbConnection implements ServiceConnection {
- volatile IObbBackupService mService;
-
- FullBackupObbConnection() {
- mService = null;
- }
-
- public void establish() {
- if (MORE_DEBUG) Slog.i(TAG, "Initiating bind of OBB service on " + this);
- Intent obbIntent = new Intent().setComponent(new ComponentName(
- "com.android.sharedstoragebackup",
- "com.android.sharedstoragebackup.ObbBackupService"));
- BackupManagerService.this.mContext.bindServiceAsUser(
- obbIntent, this, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
- }
-
- public void tearDown() {
- BackupManagerService.this.mContext.unbindService(this);
- }
-
- public boolean backupObbs(PackageInfo pkg, OutputStream out) {
- boolean success = false;
- waitForConnection();
-
- ParcelFileDescriptor[] pipes = null;
- try {
- pipes = ParcelFileDescriptor.createPipe();
- int token = generateRandomIntegerToken();
- prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL,
- null, OP_TYPE_BACKUP_WAIT);
- mService.backupObbs(pkg.packageName, pipes[1], token, mBackupManagerBinder);
- routeSocketDataToOutput(pipes[0], out);
- success = waitUntilOperationComplete(token);
- } catch (Exception e) {
- Slog.w(TAG, "Unable to back up OBBs for " + pkg, e);
- } finally {
- try {
- out.flush();
- if (pipes != null) {
- if (pipes[0] != null) pipes[0].close();
- if (pipes[1] != null) pipes[1].close();
- }
- } catch (IOException e) {
- Slog.w(TAG, "I/O error closing down OBB backup", e);
- }
- }
- return success;
- }
-
- public void restoreObbFile(String pkgName, ParcelFileDescriptor data,
- long fileSize, int type, String path, long mode, long mtime,
- int token, IBackupManager callbackBinder) {
- waitForConnection();
-
- try {
- mService.restoreObbFile(pkgName, data, fileSize, type, path, mode, mtime,
- token, callbackBinder);
- } catch (Exception e) {
- Slog.w(TAG, "Unable to restore OBBs for " + pkgName, e);
- }
- }
-
- private void waitForConnection() {
- synchronized (this) {
- while (mService == null) {
- if (MORE_DEBUG) Slog.i(TAG, "...waiting for OBB service binding...");
- try {
- this.wait();
- } catch (InterruptedException e) { /* never interrupted */ }
- }
- if (MORE_DEBUG) Slog.i(TAG, "Connected to OBB service; continuing");
- }
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- synchronized (this) {
- mService = IObbBackupService.Stub.asInterface(service);
- if (MORE_DEBUG) Slog.i(TAG, "OBB service connection " + mService
- + " connected on " + this);
- this.notifyAll();
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- synchronized (this) {
- mService = null;
- if (MORE_DEBUG) Slog.i(TAG, "OBB service connection disconnected on " + this);
- this.notifyAll();
- }
- }
-
- }
-
- static void routeSocketDataToOutput(ParcelFileDescriptor inPipe, OutputStream out)
- throws IOException {
- // We do not take close() responsibility for the pipe FD
- FileInputStream raw = new FileInputStream(inPipe.getFileDescriptor());
- DataInputStream in = new DataInputStream(raw);
-
- byte[] buffer = new byte[32 * 1024];
- int chunkTotal;
- while ((chunkTotal = in.readInt()) > 0) {
- while (chunkTotal > 0) {
- int toRead = (chunkTotal > buffer.length) ? buffer.length : chunkTotal;
- int nRead = in.read(buffer, 0, toRead);
- out.write(buffer, 0, nRead);
- chunkTotal -= nRead;
- }
- }
- }
-
- @Override
- public void tearDownAgentAndKill(ApplicationInfo app) {
- if (app == null) {
- // Null means the system package, so just quietly move on. :)
- return;
- }
-
- try {
- // unbind and tidy up even on timeout or failure, just in case
- mActivityManager.unbindBackupAgent(app);
-
- // The agent was running with a stub Application object, so shut it down.
- // !!! We hardcode the confirmation UI's package name here rather than use a
- // manifest flag! TODO something less direct.
- if (app.uid >= Process.FIRST_APPLICATION_UID
- && !app.packageName.equals("com.android.backupconfirm")) {
- if (MORE_DEBUG) Slog.d(TAG, "Killing agent host process");
- mActivityManager.killApplicationProcess(app.processName, app.uid);
- } else {
- if (MORE_DEBUG) Slog.d(TAG, "Not killing after operation: " + app.processName);
- }
- } catch (RemoteException e) {
- Slog.d(TAG, "Lost app trying to shut down");
- }
- }
-
- // Core logic for performing one package's full backup, gathering the tarball from the
- // application and emitting it to the designated OutputStream.
-
- // Callout from the engine to an interested participant that might need to communicate
- // with the agent prior to asking it to move data
- interface FullBackupPreflight {
- /**
- * Perform the preflight operation necessary for the given package.
- * @param pkg The name of the package being proposed for full-data backup
- * @param agent Live BackupAgent binding to the target app's agent
- * @return BackupTransport.TRANSPORT_OK to proceed with the backup operation,
- * or one of the other BackupTransport.* error codes as appropriate
- */
- int preflightFullBackup(PackageInfo pkg, IBackupAgent agent);
-
- long getExpectedSizeOrErrorCode();
- };
-
- class FullBackupEngine {
- OutputStream mOutput;
- FullBackupPreflight mPreflightHook;
- BackupRestoreTask mTimeoutMonitor;
- IBackupAgent mAgent;
- File mFilesDir;
- File mManifestFile;
- File mMetadataFile;
- boolean mIncludeApks;
- PackageInfo mPkg;
- private final long mQuota;
- private final int mOpToken;
-
- class FullBackupRunner implements Runnable {
- PackageInfo mPackage;
- byte[] mWidgetData;
- IBackupAgent mAgent;
- ParcelFileDescriptor mPipe;
- int mToken;
- boolean mSendApk;
- boolean mWriteManifest;
-
- FullBackupRunner(PackageInfo pack, IBackupAgent agent, ParcelFileDescriptor pipe,
- int token, boolean sendApk, boolean writeManifest, byte[] widgetData)
- throws IOException {
- mPackage = pack;
- mWidgetData = widgetData;
- mAgent = agent;
- mPipe = ParcelFileDescriptor.dup(pipe.getFileDescriptor());
- mToken = token;
- mSendApk = sendApk;
- mWriteManifest = writeManifest;
- }
-
- @Override
- public void run() {
- try {
- FullBackupDataOutput output = new FullBackupDataOutput(mPipe);
-
- if (mWriteManifest) {
- final boolean writeWidgetData = mWidgetData != null;
- if (MORE_DEBUG) Slog.d(TAG, "Writing manifest for " + mPackage.packageName);
- writeAppManifest(mPackage, mPackageManager, mManifestFile, mSendApk, writeWidgetData);
- FullBackup.backupToTar(mPackage.packageName, null, null,
- mFilesDir.getAbsolutePath(),
- mManifestFile.getAbsolutePath(),
- output);
- mManifestFile.delete();
-
- // We only need to write a metadata file if we have widget data to stash
- if (writeWidgetData) {
- writeMetadata(mPackage, mMetadataFile, mWidgetData);
- FullBackup.backupToTar(mPackage.packageName, null, null,
- mFilesDir.getAbsolutePath(),
- mMetadataFile.getAbsolutePath(),
- output);
- mMetadataFile.delete();
- }
- }
-
- if (mSendApk) {
- writeApkToBackup(mPackage, output);
- }
-
- final boolean isSharedStorage =
- mPackage.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE);
- final long timeout = isSharedStorage ?
- TIMEOUT_SHARED_BACKUP_INTERVAL : TIMEOUT_FULL_BACKUP_INTERVAL;
-
- if (DEBUG) Slog.d(TAG, "Calling doFullBackup() on " + mPackage.packageName);
- prepareOperationTimeout(mToken, timeout, mTimeoutMonitor /* in parent class */,
- OP_TYPE_BACKUP_WAIT);
- mAgent.doFullBackup(mPipe, mQuota, mToken, mBackupManagerBinder);
- } catch (IOException e) {
- Slog.e(TAG, "Error running full backup for " + mPackage.packageName);
- } catch (RemoteException e) {
- Slog.e(TAG, "Remote agent vanished during full backup of "
- + mPackage.packageName);
- } finally {
- try {
- mPipe.close();
- } catch (IOException e) {}
- }
- }
- }
-
- FullBackupEngine(OutputStream output, FullBackupPreflight preflightHook, PackageInfo pkg,
- boolean alsoApks, BackupRestoreTask timeoutMonitor, long quota, int opToken) {
- mOutput = output;
- mPreflightHook = preflightHook;
- mPkg = pkg;
- mIncludeApks = alsoApks;
- mTimeoutMonitor = timeoutMonitor;
- mFilesDir = new File("/data/system");
- mManifestFile = new File(mFilesDir, BACKUP_MANIFEST_FILENAME);
- mMetadataFile = new File(mFilesDir, BACKUP_METADATA_FILENAME);
- mQuota = quota;
- mOpToken = opToken;
- }
-
- public int preflightCheck() throws RemoteException {
- if (mPreflightHook == null) {
- if (MORE_DEBUG) {
- Slog.v(TAG, "No preflight check");
- }
- return BackupTransport.TRANSPORT_OK;
- }
- if (initializeAgent()) {
- int result = mPreflightHook.preflightFullBackup(mPkg, mAgent);
- if (MORE_DEBUG) {
- Slog.v(TAG, "preflight returned " + result);
- }
- return result;
- } else {
- Slog.w(TAG, "Unable to bind to full agent for " + mPkg.packageName);
- return BackupTransport.AGENT_ERROR;
- }
- }
-
- public int backupOnePackage() throws RemoteException {
- int result = BackupTransport.AGENT_ERROR;
-
- if (initializeAgent()) {
- ParcelFileDescriptor[] pipes = null;
- try {
- pipes = ParcelFileDescriptor.createPipe();
-
- ApplicationInfo app = mPkg.applicationInfo;
- final boolean isSharedStorage =
- mPkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE);
- final boolean sendApk = mIncludeApks
- && !isSharedStorage
- && ((app.privateFlags & ApplicationInfo.PRIVATE_FLAG_FORWARD_LOCK) == 0)
- && ((app.flags & ApplicationInfo.FLAG_SYSTEM) == 0 ||
- (app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
-
- // TODO: http://b/22388012
- byte[] widgetBlob = AppWidgetBackupBridge.getWidgetState(mPkg.packageName,
- UserHandle.USER_SYSTEM);
-
- FullBackupRunner runner = new FullBackupRunner(mPkg, mAgent, pipes[1],
- mOpToken, sendApk, !isSharedStorage, widgetBlob);
- pipes[1].close(); // the runner has dup'd it
- pipes[1] = null;
- Thread t = new Thread(runner, "app-data-runner");
- t.start();
-
- // Now pull data from the app and stuff it into the output
- routeSocketDataToOutput(pipes[0], mOutput);
-
- if (!waitUntilOperationComplete(mOpToken)) {
- Slog.e(TAG, "Full backup failed on package " + mPkg.packageName);
- } else {
- if (MORE_DEBUG) {
- Slog.d(TAG, "Full package backup success: " + mPkg.packageName);
- }
- result = BackupTransport.TRANSPORT_OK;
- }
- } catch (IOException e) {
- Slog.e(TAG, "Error backing up " + mPkg.packageName + ": " + e.getMessage());
- result = BackupTransport.AGENT_ERROR;
- } finally {
- try {
- // flush after every package
- mOutput.flush();
- if (pipes != null) {
- if (pipes[0] != null) pipes[0].close();
- if (pipes[1] != null) pipes[1].close();
- }
- } catch (IOException e) {
- Slog.w(TAG, "Error bringing down backup stack");
- result = BackupTransport.TRANSPORT_ERROR;
- }
- }
- } else {
- Slog.w(TAG, "Unable to bind to full agent for " + mPkg.packageName);
- }
- tearDown();
- return result;
- }
-
- public void sendQuotaExceeded(final long backupDataBytes, final long quotaBytes) {
- if (initializeAgent()) {
- try {
- mAgent.doQuotaExceeded(backupDataBytes, quotaBytes);
- } catch (RemoteException e) {
- Slog.e(TAG, "Remote exception while telling agent about quota exceeded");
- }
- }
- }
-
- private boolean initializeAgent() {
- if (mAgent == null) {
- if (MORE_DEBUG) {
- Slog.d(TAG, "Binding to full backup agent : " + mPkg.packageName);
- }
- mAgent = bindToAgentSynchronous(mPkg.applicationInfo,
- ApplicationThreadConstants.BACKUP_MODE_FULL);
- }
- return mAgent != null;
- }
-
- private void writeApkToBackup(PackageInfo pkg, FullBackupDataOutput output) {
- // Forward-locked apps, system-bundled .apks, etc are filtered out before we get here
- // TODO: handle backing up split APKs
- final String appSourceDir = pkg.applicationInfo.getBaseCodePath();
- final String apkDir = new File(appSourceDir).getParent();
- FullBackup.backupToTar(pkg.packageName, FullBackup.APK_TREE_TOKEN, null,
- apkDir, appSourceDir, output);
-
- // TODO: migrate this to SharedStorageBackup, since AID_SYSTEM
- // doesn't have access to external storage.
-
- // Save associated .obb content if it exists and we did save the apk
- // check for .obb and save those too
- // TODO: http://b/22388012
- final UserEnvironment userEnv = new UserEnvironment(UserHandle.USER_SYSTEM);
- final File obbDir = userEnv.buildExternalStorageAppObbDirs(pkg.packageName)[0];
- if (obbDir != null) {
- if (MORE_DEBUG) Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath());
- File[] obbFiles = obbDir.listFiles();
- if (obbFiles != null) {
- final String obbDirName = obbDir.getAbsolutePath();
- for (File obb : obbFiles) {
- FullBackup.backupToTar(pkg.packageName, FullBackup.OBB_TREE_TOKEN, null,
- obbDirName, obb.getAbsolutePath(), output);
- }
- }
- }
- }
-
- // Widget metadata format. All header entries are strings ending in LF:
- //
- // Version 1 header:
- // BACKUP_METADATA_VERSION, currently "1"
- // package name
- //
- // File data (all integers are binary in network byte order)
- // *N: 4 : integer token identifying which metadata blob
- // 4 : integer size of this blob = N
- // N : raw bytes of this metadata blob
- //
- // Currently understood blobs (always in network byte order):
- //
- // widgets : metadata token = 0x01FFED01 (BACKUP_WIDGET_METADATA_TOKEN)
- //
- // Unrecognized blobs are *ignored*, not errors.
- private void writeMetadata(PackageInfo pkg, File destination, byte[] widgetData)
- throws IOException {
- StringBuilder b = new StringBuilder(512);
- StringBuilderPrinter printer = new StringBuilderPrinter(b);
- printer.println(Integer.toString(BACKUP_METADATA_VERSION));
- printer.println(pkg.packageName);
-
- FileOutputStream fout = new FileOutputStream(destination);
- BufferedOutputStream bout = new BufferedOutputStream(fout);
- DataOutputStream out = new DataOutputStream(bout);
- bout.write(b.toString().getBytes()); // bypassing DataOutputStream
-
- if (widgetData != null && widgetData.length > 0) {
- out.writeInt(BACKUP_WIDGET_METADATA_TOKEN);
- out.writeInt(widgetData.length);
- out.write(widgetData);
- }
- bout.flush();
- out.close();
-
- // As with the manifest file, guarantee idempotence of the archive metadata
- // for the widget block by using a fixed mtime on the transient file.
- destination.setLastModified(0);
- }
-
- private void tearDown() {
- if (mPkg != null) {
- tearDownAgentAndKill(mPkg.applicationInfo);
- }
- }
- }
-
- static void writeAppManifest(PackageInfo pkg, PackageManager packageManager, File manifestFile,
- boolean withApk, boolean withWidgets) throws IOException {
- // Manifest format. All data are strings ending in LF:
- // BACKUP_MANIFEST_VERSION, currently 1
- //
- // Version 1:
- // package name
- // package's versionCode
- // platform versionCode
- // getInstallerPackageName() for this package (maybe empty)
- // boolean: "1" if archive includes .apk; any other string means not
- // number of signatures == N
- // N*: signature byte array in ascii format per Signature.toCharsString()
- StringBuilder builder = new StringBuilder(4096);
- StringBuilderPrinter printer = new StringBuilderPrinter(builder);
-
- printer.println(Integer.toString(BACKUP_MANIFEST_VERSION));
- printer.println(pkg.packageName);
- printer.println(Integer.toString(pkg.versionCode));
- printer.println(Integer.toString(Build.VERSION.SDK_INT));
-
- String installerName = packageManager.getInstallerPackageName(pkg.packageName);
- printer.println((installerName != null) ? installerName : "");
-
- printer.println(withApk ? "1" : "0");
- if (pkg.signatures == null) {
- printer.println("0");
- } else {
- printer.println(Integer.toString(pkg.signatures.length));
- for (Signature sig : pkg.signatures) {
- printer.println(sig.toCharsString());
- }
- }
-
- FileOutputStream outstream = new FileOutputStream(manifestFile);
- outstream.write(builder.toString().getBytes());
- outstream.close();
-
- // We want the manifest block in the archive stream to be idempotent:
- // each time we generate a backup stream for the app, we want the manifest
- // block to be identical. The underlying tar mechanism sees it as a file,
- // though, and will propagate its mtime, causing the tar header to vary.
- // Avoid this problem by pinning the mtime to zero.
- manifestFile.setLastModified(0);
- }
-
- // Generic driver skeleton for full backup operations
- abstract class FullBackupTask implements Runnable {
- IFullBackupRestoreObserver mObserver;
-
- FullBackupTask(IFullBackupRestoreObserver observer) {
- mObserver = observer;
- }
-
- // wrappers for observer use
- final void sendStartBackup() {
- if (mObserver != null) {
- try {
- mObserver.onStartBackup();
- } catch (RemoteException e) {
- Slog.w(TAG, "full backup observer went away: startBackup");
- mObserver = null;
- }
- }
- }
-
- final void sendOnBackupPackage(String name) {
- if (mObserver != null) {
- try {
- // TODO: use a more user-friendly name string
- mObserver.onBackupPackage(name);
- } catch (RemoteException e) {
- Slog.w(TAG, "full backup observer went away: backupPackage");
- mObserver = null;
- }
- }
- }
-
- final void sendEndBackup() {
- if (mObserver != null) {
- try {
- mObserver.onEndBackup();
- } catch (RemoteException e) {
- Slog.w(TAG, "full backup observer went away: endBackup");
- mObserver = null;
- }
- }
- }
- }
-
- boolean deviceIsEncrypted() {
- try {
- return mStorageManager.getEncryptionState()
- != StorageManager.ENCRYPTION_STATE_NONE
- && mStorageManager.getPasswordType()
- != StorageManager.CRYPT_TYPE_DEFAULT;
- } catch (Exception e) {
- // If we can't talk to the storagemanager service we have a serious problem; fail
- // "secure" i.e. assuming that the device is encrypted.
- Slog.e(TAG, "Unable to communicate with storagemanager service: " + e.getMessage());
- return true;
- }
- }
-
- // Full backup task variant used for adb backup
- class PerformAdbBackupTask extends FullBackupTask implements BackupRestoreTask {
- FullBackupEngine mBackupEngine;
- final AtomicBoolean mLatch;
-
- ParcelFileDescriptor mOutputFile;
- DeflaterOutputStream mDeflater;
- boolean mIncludeApks;
- boolean mIncludeObbs;
- boolean mIncludeShared;
- boolean mDoWidgets;
- boolean mAllApps;
- boolean mIncludeSystem;
- boolean mCompress;
- boolean mKeyValue;
- ArrayList<String> mPackages;
- PackageInfo mCurrentTarget;
- String mCurrentPassword;
- String mEncryptPassword;
- private final int mCurrentOpToken;
-
- PerformAdbBackupTask(ParcelFileDescriptor fd, IFullBackupRestoreObserver observer,
- boolean includeApks, boolean includeObbs, boolean includeShared, boolean doWidgets,
- String curPassword, String encryptPassword, boolean doAllApps, boolean doSystem,
- boolean doCompress, boolean doKeyValue, String[] packages, AtomicBoolean latch) {
- super(observer);
- mCurrentOpToken = generateRandomIntegerToken();
- mLatch = latch;
-
- mOutputFile = fd;
- mIncludeApks = includeApks;
- mIncludeObbs = includeObbs;
- mIncludeShared = includeShared;
- mDoWidgets = doWidgets;
- mAllApps = doAllApps;
- mIncludeSystem = doSystem;
- mPackages = (packages == null)
- ? new ArrayList<String>()
- : new ArrayList<String>(Arrays.asList(packages));
- mCurrentPassword = curPassword;
- // when backing up, if there is a current backup password, we require that
- // the user use a nonempty encryption password as well. if one is supplied
- // in the UI we use that, but if the UI was left empty we fall back to the
- // current backup password (which was supplied by the user as well).
- if (encryptPassword == null || "".equals(encryptPassword)) {
- mEncryptPassword = curPassword;
- } else {
- mEncryptPassword = encryptPassword;
- }
- if (MORE_DEBUG) {
- Slog.w(TAG, "Encrypting backup with passphrase=" + mEncryptPassword);
- }
- mCompress = doCompress;
- mKeyValue = doKeyValue;
- }
-
- void addPackagesToSet(TreeMap<String, PackageInfo> set, List<String> pkgNames) {
- for (String pkgName : pkgNames) {
- if (!set.containsKey(pkgName)) {
- try {
- PackageInfo info = mPackageManager.getPackageInfo(pkgName,
- PackageManager.GET_SIGNATURES);
- set.put(pkgName, info);
- } catch (NameNotFoundException e) {
- Slog.w(TAG, "Unknown package " + pkgName + ", skipping");
- }
- }
- }
- }
-
- private OutputStream emitAesBackupHeader(StringBuilder headerbuf,
- OutputStream ofstream) throws Exception {
- // User key will be used to encrypt the master key.
- byte[] newUserSalt = randomBytes(PBKDF2_SALT_SIZE);
- SecretKey userKey = buildPasswordKey(PBKDF_CURRENT, mEncryptPassword, newUserSalt,
- PBKDF2_HASH_ROUNDS);
-
- // the master key is random for each backup
- byte[] masterPw = new byte[256 / 8];
- mRng.nextBytes(masterPw);
- byte[] checksumSalt = randomBytes(PBKDF2_SALT_SIZE);
-
- // primary encryption of the datastream with the random key
- Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
- SecretKeySpec masterKeySpec = new SecretKeySpec(masterPw, "AES");
- c.init(Cipher.ENCRYPT_MODE, masterKeySpec);
- OutputStream finalOutput = new CipherOutputStream(ofstream, c);
-
- // line 4: name of encryption algorithm
- headerbuf.append(ENCRYPTION_ALGORITHM_NAME);
- headerbuf.append('\n');
- // line 5: user password salt [hex]
- headerbuf.append(byteArrayToHex(newUserSalt));
- headerbuf.append('\n');
- // line 6: master key checksum salt [hex]
- headerbuf.append(byteArrayToHex(checksumSalt));
- headerbuf.append('\n');
- // line 7: number of PBKDF2 rounds used [decimal]
- headerbuf.append(PBKDF2_HASH_ROUNDS);
- headerbuf.append('\n');
-
- // line 8: IV of the user key [hex]
- Cipher mkC = Cipher.getInstance("AES/CBC/PKCS5Padding");
- mkC.init(Cipher.ENCRYPT_MODE, userKey);
-
- byte[] IV = mkC.getIV();
- headerbuf.append(byteArrayToHex(IV));
- headerbuf.append('\n');
-
- // line 9: master IV + key blob, encrypted by the user key [hex]. Blob format:
- // [byte] IV length = Niv
- // [array of Niv bytes] IV itself
- // [byte] master key length = Nmk
- // [array of Nmk bytes] master key itself
- // [byte] MK checksum hash length = Nck
- // [array of Nck bytes] master key checksum hash
- //
- // The checksum is the (master key + checksum salt), run through the
- // stated number of PBKDF2 rounds
- IV = c.getIV();
- byte[] mk = masterKeySpec.getEncoded();
- byte[] checksum = makeKeyChecksum(PBKDF_CURRENT, masterKeySpec.getEncoded(),
- checksumSalt, PBKDF2_HASH_ROUNDS);
-
- ByteArrayOutputStream blob = new ByteArrayOutputStream(IV.length + mk.length
- + checksum.length + 3);
- DataOutputStream mkOut = new DataOutputStream(blob);
- mkOut.writeByte(IV.length);
- mkOut.write(IV);
- mkOut.writeByte(mk.length);
- mkOut.write(mk);
- mkOut.writeByte(checksum.length);
- mkOut.write(checksum);
- mkOut.flush();
- byte[] encryptedMk = mkC.doFinal(blob.toByteArray());
- headerbuf.append(byteArrayToHex(encryptedMk));
- headerbuf.append('\n');
-
- return finalOutput;
- }
-
- private void finalizeBackup(OutputStream out) {
- try {
- // A standard 'tar' EOF sequence: two 512-byte blocks of all zeroes.
- byte[] eof = new byte[512 * 2]; // newly allocated == zero filled
- out.write(eof);
- } catch (IOException e) {
- Slog.w(TAG, "Error attempting to finalize backup stream");
- }
- }
-
- @Override
- public void run() {
- String includeKeyValue = mKeyValue ? ", including key-value backups" : "";
- Slog.i(TAG, "--- Performing adb backup" + includeKeyValue + " ---");
-
- TreeMap<String, PackageInfo> packagesToBackup = new TreeMap<String, PackageInfo>();
- FullBackupObbConnection obbConnection = new FullBackupObbConnection();
- obbConnection.establish(); // we'll want this later
-
- sendStartBackup();
-
- // doAllApps supersedes the package set if any
- if (mAllApps) {
- List<PackageInfo> allPackages = mPackageManager.getInstalledPackages(
- PackageManager.GET_SIGNATURES);
- for (int i = 0; i < allPackages.size(); i++) {
- PackageInfo pkg = allPackages.get(i);
- // Exclude system apps if we've been asked to do so
- if (mIncludeSystem == true
- || ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0)) {
- packagesToBackup.put(pkg.packageName, pkg);
- }
- }
- }
-
- // If we're doing widget state as well, ensure that we have all the involved
- // host & provider packages in the set
- if (mDoWidgets) {
- // TODO: http://b/22388012
- List<String> pkgs =
- AppWidgetBackupBridge.getWidgetParticipants(UserHandle.USER_SYSTEM);
- if (pkgs != null) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Adding widget participants to backup set:");
- StringBuilder sb = new StringBuilder(128);
- sb.append(" ");
- for (String s : pkgs) {
- sb.append(' ');
- sb.append(s);
- }
- Slog.i(TAG, sb.toString());
- }
- addPackagesToSet(packagesToBackup, pkgs);
- }
- }
-
- // Now process the command line argument packages, if any. Note that explicitly-
- // named system-partition packages will be included even if includeSystem was
- // set to false.
- if (mPackages != null) {
- addPackagesToSet(packagesToBackup, mPackages);
- }
-
- // Now we cull any inapplicable / inappropriate packages from the set. This
- // includes the special shared-storage agent package; we handle that one
- // explicitly at the end of the backup pass. Packages supporting key-value backup are
- // added to their own queue, and handled after packages supporting fullbackup.
- ArrayList<PackageInfo> keyValueBackupQueue = new ArrayList<>();
- Iterator<Entry<String, PackageInfo>> iter = packagesToBackup.entrySet().iterator();
- while (iter.hasNext()) {
- PackageInfo pkg = iter.next().getValue();
- if (!appIsEligibleForBackup(pkg.applicationInfo, mPackageManager)
- || appIsStopped(pkg.applicationInfo)) {
- iter.remove();
- if (DEBUG) {
- Slog.i(TAG, "Package " + pkg.packageName
- + " is not eligible for backup, removing.");
- }
- } else if (appIsKeyValueOnly(pkg)) {
- iter.remove();
- if (DEBUG) {
- Slog.i(TAG, "Package " + pkg.packageName
- + " is key-value.");
- }
- keyValueBackupQueue.add(pkg);
- }
- }
-
- // flatten the set of packages now so we can explicitly control the ordering
- ArrayList<PackageInfo> backupQueue =
- new ArrayList<PackageInfo>(packagesToBackup.values());
- FileOutputStream ofstream = new FileOutputStream(mOutputFile.getFileDescriptor());
- OutputStream out = null;
-
- PackageInfo pkg = null;
- try {
- boolean encrypting = (mEncryptPassword != null && mEncryptPassword.length() > 0);
-
- // Only allow encrypted backups of encrypted devices
- if (deviceIsEncrypted() && !encrypting) {
- Slog.e(TAG, "Unencrypted backup of encrypted device; aborting");
- return;
- }
-
- OutputStream finalOutput = ofstream;
-
- // Verify that the given password matches the currently-active
- // backup password, if any
- if (!backupPasswordMatches(mCurrentPassword)) {
- if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
- return;
- }
-
- // Write the global file header. All strings are UTF-8 encoded; lines end
- // with a '\n' byte. Actual backup data begins immediately following the
- // final '\n'.
- //
- // line 1: "ANDROID BACKUP"
- // line 2: backup file format version, currently "5"
- // line 3: compressed? "0" if not compressed, "1" if compressed.
- // line 4: name of encryption algorithm [currently only "none" or "AES-256"]
- //
- // When line 4 is not "none", then additional header data follows:
- //
- // line 5: user password salt [hex]
- // line 6: master key checksum salt [hex]
- // line 7: number of PBKDF2 rounds to use (same for user & master) [decimal]
- // line 8: IV of the user key [hex]
- // line 9: master key blob [hex]
- // IV of the master key, master key itself, master key checksum hash
- //
- // The master key checksum is the master key plus its checksum salt, run through
- // 10k rounds of PBKDF2. This is used to verify that the user has supplied the
- // correct password for decrypting the archive: the master key decrypted from
- // the archive using the user-supplied password is also run through PBKDF2 in
- // this way, and if the result does not match the checksum as stored in the
- // archive, then we know that the user-supplied password does not match the
- // archive's.
- StringBuilder headerbuf = new StringBuilder(1024);
-
- headerbuf.append(BACKUP_FILE_HEADER_MAGIC);
- headerbuf.append(BACKUP_FILE_VERSION); // integer, no trailing \n
- headerbuf.append(mCompress ? "\n1\n" : "\n0\n");
-
- try {
- // Set up the encryption stage if appropriate, and emit the correct header
- if (encrypting) {
- finalOutput = emitAesBackupHeader(headerbuf, finalOutput);
- } else {
- headerbuf.append("none\n");
- }
-
- byte[] header = headerbuf.toString().getBytes("UTF-8");
- ofstream.write(header);
-
- // Set up the compression stage feeding into the encryption stage (if any)
- if (mCompress) {
- Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
- finalOutput = new DeflaterOutputStream(finalOutput, deflater, true);
- }
-
- out = finalOutput;
- } catch (Exception e) {
- // Should never happen!
- Slog.e(TAG, "Unable to emit archive header", e);
- return;
- }
-
- // Shared storage if requested
- if (mIncludeShared) {
- try {
- pkg = mPackageManager.getPackageInfo(SHARED_BACKUP_AGENT_PACKAGE, 0);
- backupQueue.add(pkg);
- } catch (NameNotFoundException e) {
- Slog.e(TAG, "Unable to find shared-storage backup handler");
- }
- }
-
- // Now actually run the constructed backup sequence for full backup
- int N = backupQueue.size();
- for (int i = 0; i < N; i++) {
- pkg = backupQueue.get(i);
- if (DEBUG) {
- Slog.i(TAG,"--- Performing full backup for package " + pkg.packageName
- + " ---");
- }
- final boolean isSharedStorage =
- pkg.packageName.equals(SHARED_BACKUP_AGENT_PACKAGE);
-
- mBackupEngine = new FullBackupEngine(out, null, pkg, mIncludeApks, this, Long.MAX_VALUE, mCurrentOpToken);
- sendOnBackupPackage(isSharedStorage ? "Shared storage" : pkg.packageName);
-
- // Don't need to check preflight result as there is no preflight hook.
- mCurrentTarget = pkg;
- mBackupEngine.backupOnePackage();
-
- // after the app's agent runs to handle its private filesystem
- // contents, back up any OBB content it has on its behalf.
- if (mIncludeObbs) {
- boolean obbOkay = obbConnection.backupObbs(pkg, out);
- if (!obbOkay) {
- throw new RuntimeException("Failure writing OBB stack for " + pkg);
- }
- }
- }
- // And for key-value backup if enabled
- if (mKeyValue) {
- for (PackageInfo keyValuePackage : keyValueBackupQueue) {
- if (DEBUG) {
- Slog.i(TAG, "--- Performing key-value backup for package "
- + keyValuePackage.packageName + " ---");
- }
- KeyValueAdbBackupEngine kvBackupEngine =
- new KeyValueAdbBackupEngine(out, keyValuePackage,
- BackupManagerService.this,
- mPackageManager, mBaseStateDir, mDataDir);
- sendOnBackupPackage(keyValuePackage.packageName);
- kvBackupEngine.backupOnePackage();
- }
- }
-
- // Done!
- finalizeBackup(out);
- } catch (RemoteException e) {
- Slog.e(TAG, "App died during full backup");
- } catch (Exception e) {
- Slog.e(TAG, "Internal exception during full backup", e);
- } finally {
- try {
- if (out != null) {
- out.flush();
- out.close();
- }
- mOutputFile.close();
- } catch (IOException e) {
- Slog.e(TAG, "IO error closing adb backup file: " + e.getMessage());
- }
- synchronized (mLatch) {
- mLatch.set(true);
- mLatch.notifyAll();
- }
- sendEndBackup();
- obbConnection.tearDown();
- if (DEBUG) Slog.d(TAG, "Full backup pass complete.");
- mWakelock.release();
- }
- }
-
- // BackupRestoreTask methods, used for timeout handling
- @Override
- public void execute() {
- // Unused
- }
-
- @Override
- public void operationComplete(long result) {
- // Unused
- }
-
- @Override
- public void handleCancel(boolean cancelAll) {
- final PackageInfo target = mCurrentTarget;
- if (DEBUG) {
- Slog.w(TAG, "adb backup cancel of " + target);
- }
- if (target != null) {
- tearDownAgentAndKill(mCurrentTarget.applicationInfo);
- }
- removeOperation(mCurrentOpToken);
- }
- }
-
- /**
- * Full backup task extension used for transport-oriented operation.
- *
- * Flow:
- * For each requested package:
- * - Spin off a new SinglePackageBackupRunner (mBackupRunner) for the current package.
- * - Wait until preflight is complete. (mBackupRunner.getPreflightResultBlocking())
- * - If preflight data size is within limit, start reading data from agent pipe and writing
- * to transport pipe. While there is data to send, call transport.sendBackupData(int) to
- * tell the transport how many bytes to expect on its pipe.
- * - After sending all data, call transport.finishBackup() if things went well. And
- * transport.cancelFullBackup() otherwise.
- *
- * Interactions with mCurrentOperations:
- * - An entry for this object is added to mCurrentOperations for the entire lifetime of this
- * object. Used to cancel the operation.
- * - SinglePackageBackupRunner and SinglePackageBackupPreflight will put ephemeral entries
- * to get timeouts or operation complete callbacks.
- *
- * Handling cancels:
- * - The contract we provide is that the task won't interact with the transport after
- * handleCancel() is done executing.
- * - This task blocks at 3 points: 1. Preflight result check 2. Reading on agent side pipe
- * and 3. Get backup result from mBackupRunner.
- * - Bubbling up handleCancel to mBackupRunner handles all 3: 1. Calls handleCancel on the
- * preflight operation which counts down on the preflight latch. 2. Tears down the agent,
- * so read() returns -1. 3. Notifies mCurrentOpLock which unblocks
- * mBackupRunner.getBackupResultBlocking().
- */
- class PerformFullTransportBackupTask extends FullBackupTask implements BackupRestoreTask {
- static final String TAG = "PFTBT";
-
- private final Object mCancelLock = new Object();
-
- ArrayList<PackageInfo> mPackages;
- PackageInfo mCurrentPackage;
- boolean mUpdateSchedule;
- CountDownLatch mLatch;
- FullBackupJob mJob; // if a scheduled job needs to be finished afterwards
- IBackupObserver mBackupObserver;
- IBackupManagerMonitor mMonitor;
- boolean mUserInitiated;
- private volatile IBackupTransport mTransport;
- SinglePackageBackupRunner mBackupRunner;
- private final int mBackupRunnerOpToken;
-
- // This is true when a backup operation for some package is in progress.
- private volatile boolean mIsDoingBackup;
- private volatile boolean mCancelAll;
- private final int mCurrentOpToken;
-
- PerformFullTransportBackupTask(IFullBackupRestoreObserver observer,
- String[] whichPackages, boolean updateSchedule,
- FullBackupJob runningJob, CountDownLatch latch, IBackupObserver backupObserver,
- IBackupManagerMonitor monitor, boolean userInitiated) {
- super(observer);
- mUpdateSchedule = updateSchedule;
- mLatch = latch;
- mJob = runningJob;
- mPackages = new ArrayList<PackageInfo>(whichPackages.length);
- mBackupObserver = backupObserver;
- mMonitor = monitor;
- mUserInitiated = userInitiated;
- mCurrentOpToken = generateRandomIntegerToken();
- mBackupRunnerOpToken = generateRandomIntegerToken();
-
- if (isBackupOperationInProgress()) {
- if (DEBUG) {
- Slog.d(TAG, "Skipping full backup. A backup is already in progress.");
- }
- mCancelAll = true;
- return;
- }
-
- registerTask();
-
- for (String pkg : whichPackages) {
- try {
- PackageInfo info = mPackageManager.getPackageInfo(pkg,
- PackageManager.GET_SIGNATURES);
- mCurrentPackage = info;
- if (!appIsEligibleForBackup(info.applicationInfo, mPackageManager)) {
- // Cull any packages that have indicated that backups are not permitted,
- // that run as system-domain uids but do not define their own backup agents,
- // as well as any explicit mention of the 'special' shared-storage agent
- // package (we handle that one at the end).
- if (MORE_DEBUG) {
- Slog.d(TAG, "Ignoring ineligible package " + pkg);
- }
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- sendBackupOnPackageResult(mBackupObserver, pkg,
- BackupManager.ERROR_BACKUP_NOT_ALLOWED);
- continue;
- } else if (!appGetsFullBackup(info)) {
- // Cull any packages that are found in the queue but now aren't supposed
- // to get full-data backup operations.
- if (MORE_DEBUG) {
- Slog.d(TAG, "Ignoring full-data backup of key/value participant "
- + pkg);
- }
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- sendBackupOnPackageResult(mBackupObserver, pkg,
- BackupManager.ERROR_BACKUP_NOT_ALLOWED);
- continue;
- } else if (appIsStopped(info.applicationInfo)) {
- // Cull any packages in the 'stopped' state: they've either just been
- // installed or have explicitly been force-stopped by the user. In both
- // cases we do not want to launch them for backup.
- if (MORE_DEBUG) {
- Slog.d(TAG, "Ignoring stopped package " + pkg);
- }
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- sendBackupOnPackageResult(mBackupObserver, pkg,
- BackupManager.ERROR_BACKUP_NOT_ALLOWED);
- continue;
- }
- mPackages.add(info);
- } catch (NameNotFoundException e) {
- Slog.i(TAG, "Requested package " + pkg + " not found; ignoring");
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- }
- }
- }
-
- private void registerTask() {
- synchronized (mCurrentOpLock) {
- Slog.d(TAG, "backupmanager pftbt token=" + Integer.toHexString(mCurrentOpToken));
- mCurrentOperations.put(mCurrentOpToken, new Operation(OP_PENDING, this,
- OP_TYPE_BACKUP));
- }
- }
-
- private void unregisterTask() {
- removeOperation(mCurrentOpToken);
- }
-
- @Override
- public void execute() {
- // Nothing to do.
- }
-
- @Override
- public void handleCancel(boolean cancelAll) {
- synchronized (mCancelLock) {
- // We only support 'cancelAll = true' case for this task. Cancelling of a single package
-
- // due to timeout is handled by SinglePackageBackupRunner and SinglePackageBackupPreflight.
-
- if (!cancelAll) {
- Slog.wtf(TAG, "Expected cancelAll to be true.");
- }
-
- if (mCancelAll) {
- Slog.d(TAG, "Ignoring duplicate cancel call.");
- return;
- }
-
- mCancelAll = true;
- if (mIsDoingBackup) {
- BackupManagerService.this.handleCancel(mBackupRunnerOpToken, cancelAll);
- try {
- mTransport.cancelFullBackup();
- } catch (RemoteException e) {
- Slog.w(TAG, "Error calling cancelFullBackup() on transport: " + e);
- // Can't do much.
- }
- }
- }
- }
-
- @Override
- public void operationComplete(long result) {
- // Nothing to do.
- }
-
- @Override
- public void run() {
-
- // data from the app, passed to us for bridging to the transport
- ParcelFileDescriptor[] enginePipes = null;
-
- // Pipe through which we write data to the transport
- ParcelFileDescriptor[] transportPipes = null;
-
- long backoff = 0;
- int backupRunStatus = BackupManager.SUCCESS;
-
- try {
- if (!mEnabled || !mProvisioned) {
- // Backups are globally disabled, so don't proceed.
- if (DEBUG) {
- Slog.i(TAG, "full backup requested but enabled=" + mEnabled
- + " provisioned=" + mProvisioned + "; ignoring");
- }
- int monitoringEvent;
- if (mProvisioned) {
- monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED;
- } else {
- monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
- }
- mMonitor = monitorEvent(mMonitor, monitoringEvent, null,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
- mUpdateSchedule = false;
- backupRunStatus = BackupManager.ERROR_BACKUP_NOT_ALLOWED;
- return;
- }
-
- mTransport = mTransportManager.getCurrentTransportBinder();
- if (mTransport == null) {
- Slog.w(TAG, "Transport not present; full data backup not performed");
- backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
- null);
- return;
- }
-
- // Set up to send data to the transport
- final int N = mPackages.size();
- final byte[] buffer = new byte[8192];
- for (int i = 0; i < N; i++) {
- mBackupRunner = null;
- PackageInfo currentPackage = mPackages.get(i);
- String packageName = currentPackage.packageName;
- if (DEBUG) {
- Slog.i(TAG, "Initiating full-data transport backup of " + packageName
- + " token: " + mCurrentOpToken);
- }
- EventLog.writeEvent(EventLogTags.FULL_BACKUP_PACKAGE, packageName);
-
- transportPipes = ParcelFileDescriptor.createPipe();
-
- // Tell the transport the data's coming
- int flags = mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
- int backupPackageStatus;
- long quota = Long.MAX_VALUE;
- synchronized (mCancelLock) {
- if (mCancelAll) {
- break;
- }
- backupPackageStatus = mTransport.performFullBackup(currentPackage,
- transportPipes[0], flags);
-
- if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
- quota = mTransport.getBackupQuota(currentPackage.packageName,
- true /* isFullBackup */);
- // Now set up the backup engine / data source end of things
- enginePipes = ParcelFileDescriptor.createPipe();
- mBackupRunner =
- new SinglePackageBackupRunner(enginePipes[1], currentPackage,
- mTransport, quota, mBackupRunnerOpToken);
- // The runner dup'd the pipe half, so we close it here
- enginePipes[1].close();
- enginePipes[1] = null;
-
- mIsDoingBackup = true;
- }
- }
- if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
-
- // The transport has its own copy of the read end of the pipe,
- // so close ours now
- transportPipes[0].close();
- transportPipes[0] = null;
-
- // Spin off the runner to fetch the app's data and pipe it
- // into the engine pipes
- (new Thread(mBackupRunner, "package-backup-bridge")).start();
-
- // Read data off the engine pipe and pass it to the transport
- // pipe until we hit EOD on the input stream. We do not take
- // close() responsibility for these FDs into these stream wrappers.
- FileInputStream in = new FileInputStream(
- enginePipes[0].getFileDescriptor());
- FileOutputStream out = new FileOutputStream(
- transportPipes[1].getFileDescriptor());
- long totalRead = 0;
- final long preflightResult = mBackupRunner.getPreflightResultBlocking();
- // Preflight result is negative if some error happened on preflight.
- if (preflightResult < 0) {
- if (MORE_DEBUG) {
- Slog.d(TAG, "Backup error after preflight of package "
- + packageName + ": " + preflightResult
- + ", not running backup.");
- }
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- putMonitoringExtra(null,
- BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR,
- preflightResult));
- backupPackageStatus = (int) preflightResult;
- } else {
- int nRead = 0;
- do {
- nRead = in.read(buffer);
- if (MORE_DEBUG) {
- Slog.v(TAG, "in.read(buffer) from app: " + nRead);
- }
- if (nRead > 0) {
- out.write(buffer, 0, nRead);
- synchronized (mCancelLock) {
- if (!mCancelAll) {
- backupPackageStatus = mTransport.sendBackupData(nRead);
- }
- }
- totalRead += nRead;
- if (mBackupObserver != null && preflightResult > 0) {
- sendBackupOnUpdate(mBackupObserver, packageName,
- new BackupProgress(preflightResult, totalRead));
- }
- }
- } while (nRead > 0
- && backupPackageStatus == BackupTransport.TRANSPORT_OK);
- // Despite preflight succeeded, package still can hit quota on flight.
- if (backupPackageStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
- Slog.w(TAG, "Package hit quota limit in-flight " + packageName
- + ": " + totalRead + " of " + quota);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
- null);
- mBackupRunner.sendQuotaExceeded(totalRead, quota);
- }
- }
-
- final int backupRunnerResult = mBackupRunner.getBackupResultBlocking();
-
- synchronized (mCancelLock) {
- mIsDoingBackup = false;
- // If mCancelCurrent is true, we have already called cancelFullBackup().
- if (!mCancelAll) {
- if (backupRunnerResult == BackupTransport.TRANSPORT_OK) {
- // If we were otherwise in a good state, now interpret the final
- // result based on what finishBackup() returns. If we're in a
- // failure case already, preserve that result and ignore whatever
- // finishBackup() reports.
- final int finishResult = mTransport.finishBackup();
- if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
- backupPackageStatus = finishResult;
- }
- } else {
- mTransport.cancelFullBackup();
- }
- }
- }
-
- // A transport-originated error here means that we've hit an error that the
- // runner doesn't know about, so it's still moving data but we're pulling the
- // rug out from under it. Don't ask for its result: we already know better
- // and we'll hang if we block waiting for it, since it relies on us to
- // read back the data it's writing into the engine. Just proceed with
- // a graceful failure. The runner/engine mechanism will tear itself
- // down cleanly when we close the pipes from this end. Transport-level
- // errors take precedence over agent/app-specific errors for purposes of
- // determining our course of action.
- if (backupPackageStatus == BackupTransport.TRANSPORT_OK) {
- // We still could fail in backup runner thread.
- if (backupRunnerResult != BackupTransport.TRANSPORT_OK) {
- // If there was an error in runner thread and
- // not TRANSPORT_ERROR here, overwrite it.
- backupPackageStatus = backupRunnerResult;
- }
- } else {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Transport-level failure; cancelling agent work");
- }
- }
-
- if (MORE_DEBUG) {
- Slog.i(TAG, "Done delivering backup data: result="
- + backupPackageStatus);
- }
-
- if (backupPackageStatus != BackupTransport.TRANSPORT_OK) {
- Slog.e(TAG, "Error " + backupPackageStatus + " backing up "
- + packageName);
- }
-
- // Also ask the transport how long it wants us to wait before
- // moving on to the next package, if any.
- backoff = mTransport.requestFullBackupTime();
- if (DEBUG_SCHEDULING) {
- Slog.i(TAG, "Transport suggested backoff=" + backoff);
- }
-
- }
-
- // Roll this package to the end of the backup queue if we're
- // in a queue-driven mode (regardless of success/failure)
- if (mUpdateSchedule) {
- enqueueFullBackup(packageName, System.currentTimeMillis());
- }
-
- if (backupPackageStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) {
- sendBackupOnPackageResult(mBackupObserver, packageName,
- BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED);
- if (DEBUG) {
- Slog.i(TAG, "Transport rejected backup of " + packageName
- + ", skipping");
- }
- EventLog.writeEvent(EventLogTags.FULL_BACKUP_AGENT_FAILURE, packageName,
- "transport rejected");
- // This failure state can come either a-priori from the transport, or
- // from the preflight pass. If we got as far as preflight, we now need
- // to tear down the target process.
- if (mBackupRunner != null) {
- tearDownAgentAndKill(currentPackage.applicationInfo);
- }
- // ... and continue looping.
- } else if (backupPackageStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
- sendBackupOnPackageResult(mBackupObserver, packageName,
- BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
- if (DEBUG) {
- Slog.i(TAG, "Transport quota exceeded for package: " + packageName);
- EventLog.writeEvent(EventLogTags.FULL_BACKUP_QUOTA_EXCEEDED,
- packageName);
- }
- tearDownAgentAndKill(currentPackage.applicationInfo);
- // Do nothing, clean up, and continue looping.
- } else if (backupPackageStatus == BackupTransport.AGENT_ERROR) {
- sendBackupOnPackageResult(mBackupObserver, packageName,
- BackupManager.ERROR_AGENT_FAILURE);
- Slog.w(TAG, "Application failure for package: " + packageName);
- EventLog.writeEvent(EventLogTags.BACKUP_AGENT_FAILURE, packageName);
- tearDownAgentAndKill(currentPackage.applicationInfo);
- // Do nothing, clean up, and continue looping.
- } else if (backupPackageStatus == BackupManager.ERROR_BACKUP_CANCELLED) {
- sendBackupOnPackageResult(mBackupObserver, packageName,
- BackupManager.ERROR_BACKUP_CANCELLED);
- Slog.w(TAG, "Backup cancelled. package=" + packageName +
- ", cancelAll=" + mCancelAll);
- EventLog.writeEvent(EventLogTags.FULL_BACKUP_CANCELLED, packageName);
- tearDownAgentAndKill(currentPackage.applicationInfo);
- // Do nothing, clean up, and continue looping.
- } else if (backupPackageStatus != BackupTransport.TRANSPORT_OK) {
- sendBackupOnPackageResult(mBackupObserver, packageName,
- BackupManager.ERROR_TRANSPORT_ABORTED);
- Slog.w(TAG, "Transport failed; aborting backup: " + backupPackageStatus);
- EventLog.writeEvent(EventLogTags.FULL_BACKUP_TRANSPORT_FAILURE);
- // Abort entire backup pass.
- backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
- tearDownAgentAndKill(currentPackage.applicationInfo);
- return;
- } else {
- // Success!
- sendBackupOnPackageResult(mBackupObserver, packageName,
- BackupManager.SUCCESS);
- EventLog.writeEvent(EventLogTags.FULL_BACKUP_SUCCESS, packageName);
- logBackupComplete(packageName);
- }
- cleanUpPipes(transportPipes);
- cleanUpPipes(enginePipes);
- if (currentPackage.applicationInfo != null) {
- Slog.i(TAG, "Unbinding agent in " + packageName);
- addBackupTrace("unbinding " + packageName);
- try {
- mActivityManager.unbindBackupAgent(currentPackage.applicationInfo);
- } catch (RemoteException e) { /* can't happen; activity manager is local */ }
- }
- }
- } catch (Exception e) {
- backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
- Slog.w(TAG, "Exception trying full transport backup", e);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- putMonitoringExtra(null,
- BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP,
- Log.getStackTraceString(e)));
-
- } finally {
-
- if (mCancelAll) {
- backupRunStatus = BackupManager.ERROR_BACKUP_CANCELLED;
- }
-
- if (DEBUG) {
- Slog.i(TAG, "Full backup completed with status: " + backupRunStatus);
- }
- sendBackupFinished(mBackupObserver, backupRunStatus);
-
- cleanUpPipes(transportPipes);
- cleanUpPipes(enginePipes);
-
- unregisterTask();
-
- if (mJob != null) {
- mJob.finishBackupPass();
- }
-
- synchronized (mQueueLock) {
- mRunningFullBackupTask = null;
- }
-
- mLatch.countDown();
-
- // Now that we're actually done with schedule-driven work, reschedule
- // the next pass based on the new queue state.
- if (mUpdateSchedule) {
- scheduleNextFullBackupJob(backoff);
- }
-
- Slog.i(BackupManagerService.TAG, "Full data backup pass finished.");
- mWakelock.release();
- }
- }
-
- void cleanUpPipes(ParcelFileDescriptor[] pipes) {
- if (pipes != null) {
- if (pipes[0] != null) {
- ParcelFileDescriptor fd = pipes[0];
- pipes[0] = null;
- try {
- fd.close();
- } catch (IOException e) {
- Slog.w(TAG, "Unable to close pipe!");
- }
- }
- if (pipes[1] != null) {
- ParcelFileDescriptor fd = pipes[1];
- pipes[1] = null;
- try {
- fd.close();
- } catch (IOException e) {
- Slog.w(TAG, "Unable to close pipe!");
- }
- }
- }
- }
-
- // Run the backup and pipe it back to the given socket -- expects to run on
- // a standalone thread. The runner owns this half of the pipe, and closes
- // it to indicate EOD to the other end.
- class SinglePackageBackupPreflight implements BackupRestoreTask, FullBackupPreflight {
- final AtomicLong mResult = new AtomicLong(BackupTransport.AGENT_ERROR);
- final CountDownLatch mLatch = new CountDownLatch(1);
- final IBackupTransport mTransport;
- final long mQuota;
- private final int mCurrentOpToken;
-
- SinglePackageBackupPreflight(IBackupTransport transport, long quota, int currentOpToken) {
- mTransport = transport;
- mQuota = quota;
- mCurrentOpToken = currentOpToken;
- }
-
- @Override
- public int preflightFullBackup(PackageInfo pkg, IBackupAgent agent) {
- int result;
- try {
- prepareOperationTimeout(mCurrentOpToken, TIMEOUT_FULL_BACKUP_INTERVAL,
- this, OP_TYPE_BACKUP_WAIT);
- addBackupTrace("preflighting");
- if (MORE_DEBUG) {
- Slog.d(TAG, "Preflighting full payload of " + pkg.packageName);
- }
- agent.doMeasureFullBackup(mQuota, mCurrentOpToken, mBackupManagerBinder);
-
- // Now wait to get our result back. If this backstop timeout is reached without
- // the latch being thrown, flow will continue as though a result or "normal"
- // timeout had been produced. In case of a real backstop timeout, mResult
- // will still contain the value it was constructed with, AGENT_ERROR, which
- // intentionaly falls into the "just report failure" code.
- mLatch.await(TIMEOUT_FULL_BACKUP_INTERVAL, TimeUnit.MILLISECONDS);
-
- long totalSize = mResult.get();
- // If preflight timed out, mResult will contain error code as int.
- if (totalSize < 0) {
- return (int) totalSize;
- }
- if (MORE_DEBUG) {
- Slog.v(TAG, "Got preflight response; size=" + totalSize);
- }
-
- result = mTransport.checkFullBackupSize(totalSize);
- if (result == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
- if (MORE_DEBUG) {
- Slog.d(TAG, "Package hit quota limit on preflight " +
- pkg.packageName + ": " + totalSize + " of " + mQuota);
- }
- agent.doQuotaExceeded(totalSize, mQuota);
- }
- } catch (Exception e) {
- Slog.w(TAG, "Exception preflighting " + pkg.packageName + ": " + e.getMessage());
- result = BackupTransport.AGENT_ERROR;
- }
- return result;
- }
-
- @Override
- public void execute() {
- // Unused.
- }
-
- @Override
- public void operationComplete(long result) {
- // got the callback, and our preflightFullBackup() method is waiting for the result
- if (MORE_DEBUG) {
- Slog.i(TAG, "Preflight op complete, result=" + result);
- }
- mResult.set(result);
- mLatch.countDown();
- removeOperation(mCurrentOpToken);
- }
-
- @Override
- public void handleCancel(boolean cancelAll) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Preflight cancelled; failing");
- }
- mResult.set(BackupTransport.AGENT_ERROR);
- mLatch.countDown();
- removeOperation(mCurrentOpToken);
- }
-
- @Override
- public long getExpectedSizeOrErrorCode() {
- try {
- mLatch.await(TIMEOUT_FULL_BACKUP_INTERVAL, TimeUnit.MILLISECONDS);
- return mResult.get();
- } catch (InterruptedException e) {
- return BackupTransport.NO_MORE_DATA;
- }
- }
- }
-
- class SinglePackageBackupRunner implements Runnable, BackupRestoreTask {
- final ParcelFileDescriptor mOutput;
- final PackageInfo mTarget;
- final SinglePackageBackupPreflight mPreflight;
- final CountDownLatch mPreflightLatch;
- final CountDownLatch mBackupLatch;
- private final int mCurrentOpToken;
- private final int mEphemeralToken;
- private FullBackupEngine mEngine;
- private volatile int mPreflightResult;
- private volatile int mBackupResult;
- private final long mQuota;
- private volatile boolean mIsCancelled;
-
- SinglePackageBackupRunner(ParcelFileDescriptor output, PackageInfo target,
- IBackupTransport transport, long quota, int currentOpToken) throws IOException {
- mOutput = ParcelFileDescriptor.dup(output.getFileDescriptor());
- mTarget = target;
- mCurrentOpToken = currentOpToken;
- mEphemeralToken = generateRandomIntegerToken();
- mPreflight = new SinglePackageBackupPreflight(transport, quota, mEphemeralToken);
- mPreflightLatch = new CountDownLatch(1);
- mBackupLatch = new CountDownLatch(1);
- mPreflightResult = BackupTransport.AGENT_ERROR;
- mBackupResult = BackupTransport.AGENT_ERROR;
- mQuota = quota;
- registerTask();
- }
-
- void registerTask() {
- synchronized (mCurrentOpLock) {
- mCurrentOperations.put(mCurrentOpToken, new Operation(OP_PENDING, this,
- OP_TYPE_BACKUP_WAIT));
- }
- }
-
- void unregisterTask() {
- synchronized (mCurrentOpLock) {
- mCurrentOperations.remove(mCurrentOpToken);
- }
- }
-
- @Override
- public void run() {
- FileOutputStream out = new FileOutputStream(mOutput.getFileDescriptor());
- mEngine = new FullBackupEngine(out, mPreflight, mTarget, false, this, mQuota, mCurrentOpToken);
- try {
- try {
- if (!mIsCancelled) {
- mPreflightResult = mEngine.preflightCheck();
- }
- } finally {
- mPreflightLatch.countDown();
- }
- // If there is no error on preflight, continue backup.
- if (mPreflightResult == BackupTransport.TRANSPORT_OK) {
- if (!mIsCancelled) {
- mBackupResult = mEngine.backupOnePackage();
- }
- }
- } catch (Exception e) {
- Slog.e(TAG, "Exception during full package backup of " + mTarget.packageName);
- } finally {
- unregisterTask();
- mBackupLatch.countDown();
- try {
- mOutput.close();
- } catch (IOException e) {
- Slog.w(TAG, "Error closing transport pipe in runner");
- }
- }
- }
-
- public void sendQuotaExceeded(final long backupDataBytes, final long quotaBytes) {
- mEngine.sendQuotaExceeded(backupDataBytes, quotaBytes);
- }
-
- // If preflight succeeded, returns positive number - preflight size,
- // otherwise return negative error code.
- long getPreflightResultBlocking() {
- try {
- mPreflightLatch.await(TIMEOUT_FULL_BACKUP_INTERVAL, TimeUnit.MILLISECONDS);
- if (mIsCancelled) {
- return BackupManager.ERROR_BACKUP_CANCELLED;
- }
- if (mPreflightResult == BackupTransport.TRANSPORT_OK) {
- return mPreflight.getExpectedSizeOrErrorCode();
- } else {
- return mPreflightResult;
- }
- } catch (InterruptedException e) {
- return BackupTransport.AGENT_ERROR;
- }
- }
-
- int getBackupResultBlocking() {
- try {
- mBackupLatch.await(TIMEOUT_FULL_BACKUP_INTERVAL, TimeUnit.MILLISECONDS);
- if (mIsCancelled) {
- return BackupManager.ERROR_BACKUP_CANCELLED;
- }
- return mBackupResult;
- } catch (InterruptedException e) {
- return BackupTransport.AGENT_ERROR;
- }
- }
-
-
- // BackupRestoreTask interface: specifically, timeout detection
-
- @Override
- public void execute() { /* intentionally empty */ }
-
- @Override
- public void operationComplete(long result) { /* intentionally empty */ }
-
- @Override
- public void handleCancel(boolean cancelAll) {
- if (DEBUG) {
- Slog.w(TAG, "Full backup cancel of " + mTarget.packageName);
- }
-
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
- mIsCancelled = true;
- // Cancel tasks spun off by this task.
- BackupManagerService.this.handleCancel(mEphemeralToken, cancelAll);
- tearDownAgentAndKill(mTarget.applicationInfo);
- // Free up everyone waiting on this task and its children.
- mPreflightLatch.countDown();
- mBackupLatch.countDown();
- // We are done with this operation.
- removeOperation(mCurrentOpToken);
- }
- }
- }
-
- // ----- Full-data backup scheduling -----
-
- /**
- * Schedule a job to tell us when it's a good time to run a full backup
- */
- void scheduleNextFullBackupJob(long transportMinLatency) {
- synchronized (mQueueLock) {
- if (mFullBackupQueue.size() > 0) {
- // schedule the next job at the point in the future when the least-recently
- // backed up app comes due for backup again; or immediately if it's already
- // due.
- final long upcomingLastBackup = mFullBackupQueue.get(0).lastBackup;
- final long timeSinceLast = System.currentTimeMillis() - upcomingLastBackup;
- final long interval = mConstants.getFullBackupIntervalMilliseconds();
- final long appLatency = (timeSinceLast < interval) ? (interval - timeSinceLast) : 0;
- final long latency = Math.max(transportMinLatency, appLatency);
- Runnable r = new Runnable() {
- @Override public void run() {
- FullBackupJob.schedule(mContext, latency, mConstants);
- }
- };
- mBackupHandler.postDelayed(r, 2500);
- } else {
- if (DEBUG_SCHEDULING) {
- Slog.i(TAG, "Full backup queue empty; not scheduling");
- }
- }
- }
- }
-
- /**
- * Remove a package from the full-data queue.
- */
- void dequeueFullBackupLocked(String packageName) {
- final int N = mFullBackupQueue.size();
- for (int i = N-1; i >= 0; i--) {
- final FullBackupEntry e = mFullBackupQueue.get(i);
- if (packageName.equals(e.packageName)) {
- mFullBackupQueue.remove(i);
- }
- }
- }
-
- /**
- * Enqueue full backup for the given app, with a note about when it last ran.
- */
- void enqueueFullBackup(String packageName, long lastBackedUp) {
- FullBackupEntry newEntry = new FullBackupEntry(packageName, lastBackedUp);
- synchronized (mQueueLock) {
- // First, sanity check that we aren't adding a duplicate. Slow but
- // straightforward; we'll have at most on the order of a few hundred
- // items in this list.
- dequeueFullBackupLocked(packageName);
-
- // This is also slow but easy for modest numbers of apps: work backwards
- // from the end of the queue until we find an item whose last backup
- // time was before this one, then insert this new entry after it. If we're
- // adding something new we don't bother scanning, and just prepend.
- int which = -1;
- if (lastBackedUp > 0) {
- for (which = mFullBackupQueue.size() - 1; which >= 0; which--) {
- final FullBackupEntry entry = mFullBackupQueue.get(which);
- if (entry.lastBackup <= lastBackedUp) {
- mFullBackupQueue.add(which + 1, newEntry);
- break;
- }
- }
- }
- if (which < 0) {
- // this one is earlier than any existing one, so prepend
- mFullBackupQueue.add(0, newEntry);
- }
- }
- writeFullBackupScheduleAsync();
- }
-
- private boolean fullBackupAllowable(IBackupTransport transport) {
- if (transport == null) {
- Slog.w(TAG, "Transport not present; full data backup not performed");
- return false;
- }
-
- // Don't proceed unless we have already established package metadata
- // for the current dataset via a key/value backup pass.
- try {
- File stateDir = new File(mBaseStateDir, transport.transportDirName());
- File pmState = new File(stateDir, PACKAGE_MANAGER_SENTINEL);
- if (pmState.length() <= 0) {
- if (DEBUG) {
- Slog.i(TAG, "Full backup requested but dataset not yet initialized");
- }
- return false;
- }
- } catch (Exception e) {
- Slog.w(TAG, "Unable to get transport name: " + e.getMessage());
- return false;
- }
-
- return true;
- }
-
- /**
- * Conditions are right for a full backup operation, so run one. The model we use is
- * to perform one app backup per scheduled job execution, and to reschedule the job
- * with zero latency as long as conditions remain right and we still have work to do.
- *
- * <p>This is the "start a full backup operation" entry point called by the scheduled job.
- *
- * @return Whether ongoing work will continue. The return value here will be passed
- * along as the return value to the scheduled job's onStartJob() callback.
- */
- @Override
- public boolean beginFullBackup(FullBackupJob scheduledJob) {
- final long now = System.currentTimeMillis();
- FullBackupEntry entry = null;
- final long fullBackupInterval;
- final long keyValueBackupInterval;
- synchronized (mConstants) {
- fullBackupInterval = mConstants.getFullBackupIntervalMilliseconds();
- keyValueBackupInterval = mConstants.getKeyValueBackupIntervalMilliseconds();
- }
- long latency = fullBackupInterval;
-
- if (!mEnabled || !mProvisioned) {
- // Backups are globally disabled, so don't proceed. We also don't reschedule
- // the job driving automatic backups; that job will be scheduled again when
- // the user enables backup.
- if (MORE_DEBUG) {
- Slog.i(TAG, "beginFullBackup but e=" + mEnabled
- + " p=" + mProvisioned + "; ignoring");
- }
- return false;
- }
-
- // Don't run the backup if we're in battery saver mode, but reschedule
- // to try again in the not-so-distant future.
- final PowerSaveState result =
- mPowerManager.getPowerSaveState(ServiceType.FULL_BACKUP);
- if (result.batterySaverEnabled) {
- if (DEBUG) Slog.i(TAG, "Deferring scheduled full backups in battery saver mode");
- FullBackupJob.schedule(mContext, keyValueBackupInterval, mConstants);
- return false;
- }
-
- if (DEBUG_SCHEDULING) {
- Slog.i(TAG, "Beginning scheduled full backup operation");
- }
-
- // Great; we're able to run full backup jobs now. See if we have any work to do.
- synchronized (mQueueLock) {
- if (mRunningFullBackupTask != null) {
- Slog.e(TAG, "Backup triggered but one already/still running!");
- return false;
- }
-
- // At this point we think that we have work to do, but possibly not right now.
- // Any exit without actually running backups will also require that we
- // reschedule the job.
- boolean runBackup = true;
- boolean headBusy;
-
- do {
- // Recheck each time, because culling due to ineligibility may
- // have emptied the queue.
- if (mFullBackupQueue.size() == 0) {
- // no work to do so just bow out
- if (DEBUG) {
- Slog.i(TAG, "Backup queue empty; doing nothing");
- }
- runBackup = false;
- break;
- }
-
- headBusy = false;
-
- if (!fullBackupAllowable(mTransportManager.getCurrentTransportBinder())) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Preconditions not met; not running full backup");
- }
- runBackup = false;
- // Typically this means we haven't run a key/value backup yet. Back off
- // full-backup operations by the key/value job's run interval so that
- // next time we run, we are likely to be able to make progress.
- latency = keyValueBackupInterval;
- }
-
- if (runBackup) {
- entry = mFullBackupQueue.get(0);
- long timeSinceRun = now - entry.lastBackup;
- runBackup = (timeSinceRun >= fullBackupInterval);
- if (!runBackup) {
- // It's too early to back up the next thing in the queue, so bow out
- if (MORE_DEBUG) {
- Slog.i(TAG, "Device ready but too early to back up next app");
- }
- // Wait until the next app in the queue falls due for a full data backup
- latency = fullBackupInterval - timeSinceRun;
- break; // we know we aren't doing work yet, so bail.
- }
-
- try {
- PackageInfo appInfo = mPackageManager.getPackageInfo(entry.packageName, 0);
- if (!appGetsFullBackup(appInfo)) {
- // The head app isn't supposed to get full-data backups [any more];
- // so we cull it and force a loop around to consider the new head
- // app.
- if (MORE_DEBUG) {
- Slog.i(TAG, "Culling package " + entry.packageName
- + " in full-backup queue but not eligible");
- }
- mFullBackupQueue.remove(0);
- headBusy = true; // force the while() condition
- continue;
- }
-
- final int privFlags = appInfo.applicationInfo.privateFlags;
- headBusy = (privFlags & PRIVATE_FLAG_BACKUP_IN_FOREGROUND) == 0
- && mActivityManager.isAppForeground(appInfo.applicationInfo.uid);
-
- if (headBusy) {
- final long nextEligible = System.currentTimeMillis()
- + BUSY_BACKOFF_MIN_MILLIS
- + mTokenGenerator.nextInt(BUSY_BACKOFF_FUZZ);
- if (DEBUG_SCHEDULING) {
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- Slog.i(TAG, "Full backup time but " + entry.packageName
- + " is busy; deferring to "
- + sdf.format(new Date(nextEligible)));
- }
- // This relocates the app's entry from the head of the queue to
- // its order-appropriate position further down, so upon looping
- // a new candidate will be considered at the head.
- enqueueFullBackup(entry.packageName, nextEligible - fullBackupInterval);
- }
- } catch (NameNotFoundException nnf) {
- // So, we think we want to back this up, but it turns out the package
- // in question is no longer installed. We want to drop it from the
- // queue entirely and move on, but if there's nothing else in the queue
- // we should bail entirely. headBusy cannot have been set to true yet.
- runBackup = (mFullBackupQueue.size() > 1);
- } catch (RemoteException e) {
- // Cannot happen; the Activity Manager is in the same process
- }
- }
- } while (headBusy);
-
- if (!runBackup) {
- if (DEBUG_SCHEDULING) {
- Slog.i(TAG, "Nothing pending full backup; rescheduling +" + latency);
- }
- final long deferTime = latency; // pin for the closure
- mBackupHandler.post(new Runnable() {
- @Override public void run() {
- FullBackupJob.schedule(mContext, deferTime, mConstants);
- }
- });
- return false;
- }
-
- // Okay, the top thing is ready for backup now. Do it.
- mFullBackupQueue.remove(0);
- CountDownLatch latch = new CountDownLatch(1);
- String[] pkg = new String[] {entry.packageName};
- mRunningFullBackupTask = new PerformFullTransportBackupTask(null, pkg, true,
- scheduledJob, latch, null, null, false /* userInitiated */);
- // Acquiring wakelock for PerformFullTransportBackupTask before its start.
- mWakelock.acquire();
- (new Thread(mRunningFullBackupTask)).start();
- }
-
- return true;
- }
-
- // The job scheduler says our constraints don't hold any more,
- // so tear down any ongoing backup task right away.
- @Override
- public void endFullBackup() {
- // offload the mRunningFullBackupTask.handleCancel() call to another thread,
- // as we might have to wait for mCancelLock
- Runnable endFullBackupRunnable = new Runnable() {
- @Override
- public void run() {
- PerformFullTransportBackupTask pftbt = null;
- synchronized (mQueueLock) {
- if (mRunningFullBackupTask != null) {
- pftbt = mRunningFullBackupTask;
- }
- }
- if (pftbt != null) {
- if (DEBUG_SCHEDULING) {
- Slog.i(TAG, "Telling running backup to stop");
- }
- pftbt.handleCancel(true);
- }
- }
- };
- new Thread(endFullBackupRunnable, "end-full-backup").start();
- }
-
- // ----- Restore infrastructure -----
-
- abstract class RestoreEngine {
- static final String TAG = "RestoreEngine";
-
- public static final int SUCCESS = 0;
- public static final int TARGET_FAILURE = -2;
- public static final int TRANSPORT_FAILURE = -3;
-
- private AtomicBoolean mRunning = new AtomicBoolean(false);
- private AtomicInteger mResult = new AtomicInteger(SUCCESS);
-
- public boolean isRunning() {
- return mRunning.get();
- }
-
- public void setRunning(boolean stillRunning) {
- synchronized (mRunning) {
- mRunning.set(stillRunning);
- mRunning.notifyAll();
- }
- }
-
- public int waitForResult() {
- synchronized (mRunning) {
- while (isRunning()) {
- try {
- mRunning.wait();
- } catch (InterruptedException e) {}
- }
- }
- return getResult();
- }
-
- public int getResult() {
- return mResult.get();
- }
-
- public void setResult(int result) {
- mResult.set(result);
- }
-
- // TODO: abstract restore state and APIs
- }
-
- // ----- Full restore from a file/socket -----
-
- enum RestorePolicy {
- IGNORE,
- ACCEPT,
- ACCEPT_IF_APK
- }
-
- // Full restore engine, used by both adb restore and transport-based full restore
- class FullRestoreEngine extends RestoreEngine {
- // Task in charge of monitoring timeouts
- BackupRestoreTask mMonitorTask;
-
- // Dedicated observer, if any
- IFullBackupRestoreObserver mObserver;
-
- IBackupManagerMonitor mMonitor;
-
- // Where we're delivering the file data as we go
- IBackupAgent mAgent;
-
- // Are we permitted to only deliver a specific package's metadata?
- PackageInfo mOnlyPackage;
-
- boolean mAllowApks;
- boolean mAllowObbs;
-
- // Which package are we currently handling data for?
- String mAgentPackage;
-
- // Info for working with the target app process
- ApplicationInfo mTargetApp;
-
- // Machinery for restoring OBBs
- FullBackupObbConnection mObbConnection = null;
-
- // possible handling states for a given package in the restore dataset
- final HashMap<String, RestorePolicy> mPackagePolicies
- = new HashMap<String, RestorePolicy>();
-
- // installer package names for each encountered app, derived from the manifests
- final HashMap<String, String> mPackageInstallers = new HashMap<String, String>();
-
- // Signatures for a given package found in its manifest file
- final HashMap<String, Signature[]> mManifestSignatures
- = new HashMap<String, Signature[]>();
-
- // Packages we've already wiped data on when restoring their first file
- final HashSet<String> mClearedPackages = new HashSet<String>();
-
- // How much data have we moved?
- long mBytes;
-
- // Working buffer
- byte[] mBuffer;
-
- // Pipes for moving data
- ParcelFileDescriptor[] mPipes = null;
-
- // Widget blob to be restored out-of-band
- byte[] mWidgetData = null;
-
- private final int mEphemeralOpToken;
-
- // Runner that can be placed in a separate thread to do in-process
- // invocations of the full restore API asynchronously. Used by adb restore.
- class RestoreFileRunnable implements Runnable {
- IBackupAgent mAgent;
- FileMetadata mInfo;
- ParcelFileDescriptor mSocket;
- int mToken;
-
- RestoreFileRunnable(IBackupAgent agent, FileMetadata info,
- ParcelFileDescriptor socket, int token) throws IOException {
- mAgent = agent;
- mInfo = info;
- mToken = token;
-
- // This class is used strictly for process-local binder invocations. The
- // semantics of ParcelFileDescriptor differ in this case; in particular, we
- // do not automatically get a 'dup'ed descriptor that we can can continue
- // to use asynchronously from the caller. So, we make sure to dup it ourselves
- // before proceeding to do the restore.
- mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor());
- }
-
- @Override
- public void run() {
- try {
- mAgent.doRestoreFile(mSocket, mInfo.size, mInfo.type,
- mInfo.domain, mInfo.path, mInfo.mode, mInfo.mtime,
- mToken, mBackupManagerBinder);
- } catch (RemoteException e) {
- // never happens; this is used strictly for local binder calls
- }
- }
- }
-
- public FullRestoreEngine(BackupRestoreTask monitorTask, IFullBackupRestoreObserver observer,
- IBackupManagerMonitor monitor, PackageInfo onlyPackage, boolean allowApks,
- boolean allowObbs, int ephemeralOpToken) {
- mEphemeralOpToken = ephemeralOpToken;
- mMonitorTask = monitorTask;
- mObserver = observer;
- mMonitor = monitor;
- mOnlyPackage = onlyPackage;
- mAllowApks = allowApks;
- mAllowObbs = allowObbs;
- mBuffer = new byte[32 * 1024];
- mBytes = 0;
- }
-
- public IBackupAgent getAgent() {
- return mAgent;
- }
-
- public byte[] getWidgetData() {
- return mWidgetData;
- }
-
- public boolean restoreOneFile(InputStream instream, boolean mustKillAgent) {
- if (!isRunning()) {
- Slog.w(TAG, "Restore engine used after halting");
- return false;
- }
-
- FileMetadata info;
- try {
- if (MORE_DEBUG) {
- Slog.v(TAG, "Reading tar header for restoring file");
- }
- info = readTarHeaders(instream);
- if (info != null) {
- if (MORE_DEBUG) {
- dumpFileMetadata(info);
- }
-
- final String pkg = info.packageName;
- if (!pkg.equals(mAgentPackage)) {
- // In the single-package case, it's a semantic error to expect
- // one app's data but see a different app's on the wire
- if (mOnlyPackage != null) {
- if (!pkg.equals(mOnlyPackage.packageName)) {
- Slog.w(TAG, "Expected data for " + mOnlyPackage
- + " but saw " + pkg);
- setResult(RestoreEngine.TRANSPORT_FAILURE);
- setRunning(false);
- return false;
- }
- }
-
- // okay, change in package; set up our various
- // bookkeeping if we haven't seen it yet
- if (!mPackagePolicies.containsKey(pkg)) {
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
- }
-
- // Clean up the previous agent relationship if necessary,
- // and let the observer know we're considering a new app.
- if (mAgent != null) {
- if (DEBUG) Slog.d(TAG, "Saw new package; finalizing old one");
- // Now we're really done
- tearDownPipes();
- tearDownAgent(mTargetApp);
- mTargetApp = null;
- mAgentPackage = null;
- }
- }
-
- if (info.path.equals(BACKUP_MANIFEST_FILENAME)) {
- mPackagePolicies.put(pkg, readAppManifest(info, instream));
- mPackageInstallers.put(pkg, info.installerPackageName);
- // We've read only the manifest content itself at this point,
- // so consume the footer before looping around to the next
- // input file
- skipTarPadding(info.size, instream);
- sendOnRestorePackage(pkg);
- } else if (info.path.equals(BACKUP_METADATA_FILENAME)) {
- // Metadata blobs!
- readMetadata(info, instream);
- skipTarPadding(info.size, instream);
- } else {
- // Non-manifest, so it's actual file data. Is this a package
- // we're ignoring?
- boolean okay = true;
- RestorePolicy policy = mPackagePolicies.get(pkg);
- switch (policy) {
- case IGNORE:
- okay = false;
- break;
-
- case ACCEPT_IF_APK:
- // If we're in accept-if-apk state, then the first file we
- // see MUST be the apk.
- if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) {
- if (DEBUG) Slog.d(TAG, "APK file; installing");
- // Try to install the app.
- String installerName = mPackageInstallers.get(pkg);
- okay = installApk(info, installerName, instream);
- // good to go; promote to ACCEPT
- mPackagePolicies.put(pkg, (okay)
- ? RestorePolicy.ACCEPT
- : RestorePolicy.IGNORE);
- // At this point we've consumed this file entry
- // ourselves, so just strip the tar footer and
- // go on to the next file in the input stream
- skipTarPadding(info.size, instream);
- return true;
- } else {
- // File data before (or without) the apk. We can't
- // handle it coherently in this case so ignore it.
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
- okay = false;
- }
- break;
-
- case ACCEPT:
- if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) {
- if (DEBUG) Slog.d(TAG, "apk present but ACCEPT");
- // we can take the data without the apk, so we
- // *want* to do so. skip the apk by declaring this
- // one file not-okay without changing the restore
- // policy for the package.
- okay = false;
- }
- break;
-
- default:
- // Something has gone dreadfully wrong when determining
- // the restore policy from the manifest. Ignore the
- // rest of this package's data.
- Slog.e(TAG, "Invalid policy from manifest");
- okay = false;
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
- break;
- }
-
- // Is it a *file* we need to drop?
- if (!isRestorableFile(info)) {
- okay = false;
- }
-
- // If the policy is satisfied, go ahead and set up to pipe the
- // data to the agent.
- if (MORE_DEBUG && okay && mAgent != null) {
- Slog.i(TAG, "Reusing existing agent instance");
- }
- if (okay && mAgent == null) {
- if (MORE_DEBUG) Slog.d(TAG, "Need to launch agent for " + pkg);
-
- try {
- mTargetApp = mPackageManager.getApplicationInfo(pkg, 0);
-
- // If we haven't sent any data to this app yet, we probably
- // need to clear it first. Check that.
- if (!mClearedPackages.contains(pkg)) {
- // apps with their own backup agents are
- // responsible for coherently managing a full
- // restore.
- if (mTargetApp.backupAgentName == null) {
- if (DEBUG) Slog.d(TAG, "Clearing app data preparatory to full restore");
- clearApplicationDataSynchronous(pkg);
- } else {
- if (MORE_DEBUG) Slog.d(TAG, "backup agent ("
- + mTargetApp.backupAgentName + ") => no clear");
- }
- mClearedPackages.add(pkg);
- } else {
- if (MORE_DEBUG) {
- Slog.d(TAG, "We've initialized this app already; no clear required");
- }
- }
-
- // All set; now set up the IPC and launch the agent
- setUpPipes();
- mAgent = bindToAgentSynchronous(mTargetApp,
- ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL);
- mAgentPackage = pkg;
- } catch (IOException e) {
- // fall through to error handling
- } catch (NameNotFoundException e) {
- // fall through to error handling
- }
-
- if (mAgent == null) {
- Slog.e(TAG, "Unable to create agent for " + pkg);
- okay = false;
- tearDownPipes();
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
- }
- }
-
- // Sanity check: make sure we never give data to the wrong app. This
- // should never happen but a little paranoia here won't go amiss.
- if (okay && !pkg.equals(mAgentPackage)) {
- Slog.e(TAG, "Restoring data for " + pkg
- + " but agent is for " + mAgentPackage);
- okay = false;
- }
-
- // At this point we have an agent ready to handle the full
- // restore data as well as a pipe for sending data to
- // that agent. Tell the agent to start reading from the
- // pipe.
- if (okay) {
- boolean agentSuccess = true;
- long toCopy = info.size;
- try {
- prepareOperationTimeout(mEphemeralOpToken,
- TIMEOUT_FULL_BACKUP_INTERVAL, mMonitorTask,
- OP_TYPE_RESTORE_WAIT);
-
- if (info.domain.equals(FullBackup.OBB_TREE_TOKEN)) {
- if (DEBUG) Slog.d(TAG, "Restoring OBB file for " + pkg
- + " : " + info.path);
- mObbConnection.restoreObbFile(pkg, mPipes[0],
- info.size, info.type, info.path, info.mode,
- info.mtime, mEphemeralOpToken, mBackupManagerBinder);
- } else {
- if (MORE_DEBUG) Slog.d(TAG, "Invoking agent to restore file "
- + info.path);
- // fire up the app's agent listening on the socket. If
- // the agent is running in the system process we can't
- // just invoke it asynchronously, so we provide a thread
- // for it here.
- if (mTargetApp.processName.equals("system")) {
- Slog.d(TAG, "system process agent - spinning a thread");
- RestoreFileRunnable runner = new RestoreFileRunnable(
- mAgent, info, mPipes[0], mEphemeralOpToken);
- new Thread(runner, "restore-sys-runner").start();
- } else {
- mAgent.doRestoreFile(mPipes[0], info.size, info.type,
- info.domain, info.path, info.mode, info.mtime,
- mEphemeralOpToken, mBackupManagerBinder);
- }
- }
- } catch (IOException e) {
- // couldn't dup the socket for a process-local restore
- Slog.d(TAG, "Couldn't establish restore");
- agentSuccess = false;
- okay = false;
- } catch (RemoteException e) {
- // whoops, remote entity went away. We'll eat the content
- // ourselves, then, and not copy it over.
- Slog.e(TAG, "Agent crashed during full restore");
- agentSuccess = false;
- okay = false;
- }
-
- // Copy over the data if the agent is still good
- if (okay) {
- if (MORE_DEBUG) {
- Slog.v(TAG, " copying to restore agent: "
- + toCopy + " bytes");
- }
- boolean pipeOkay = true;
- FileOutputStream pipe = new FileOutputStream(
- mPipes[1].getFileDescriptor());
- while (toCopy > 0) {
- int toRead = (toCopy > mBuffer.length)
- ? mBuffer.length : (int)toCopy;
- int nRead = instream.read(mBuffer, 0, toRead);
- if (nRead >= 0) mBytes += nRead;
- if (nRead <= 0) break;
- toCopy -= nRead;
-
- // send it to the output pipe as long as things
- // are still good
- if (pipeOkay) {
- try {
- pipe.write(mBuffer, 0, nRead);
- } catch (IOException e) {
- Slog.e(TAG, "Failed to write to restore pipe: "
- + e.getMessage());
- pipeOkay = false;
- }
- }
- }
-
- // done sending that file! Now we just need to consume
- // the delta from info.size to the end of block.
- skipTarPadding(info.size, instream);
-
- // and now that we've sent it all, wait for the remote
- // side to acknowledge receipt
- agentSuccess = waitUntilOperationComplete(mEphemeralOpToken);
- }
-
- // okay, if the remote end failed at any point, deal with
- // it by ignoring the rest of the restore on it
- if (!agentSuccess) {
- Slog.w(TAG, "Agent failure; ending restore");
- mBackupHandler.removeMessages(MSG_RESTORE_OPERATION_TIMEOUT);
- tearDownPipes();
- tearDownAgent(mTargetApp);
- mAgent = null;
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
-
- // If this was a single-package restore, we halt immediately
- // with an agent error under these circumstances
- if (mOnlyPackage != null) {
- setResult(RestoreEngine.TARGET_FAILURE);
- setRunning(false);
- return false;
- }
- }
- }
-
- // Problems setting up the agent communication, an explicitly
- // dropped file, or an already-ignored package: skip to the
- // next stream entry by reading and discarding this file.
- if (!okay) {
- if (MORE_DEBUG) Slog.d(TAG, "[discarding file content]");
- long bytesToConsume = (info.size + 511) & ~511;
- while (bytesToConsume > 0) {
- int toRead = (bytesToConsume > mBuffer.length)
- ? mBuffer.length : (int)bytesToConsume;
- long nRead = instream.read(mBuffer, 0, toRead);
- if (nRead >= 0) mBytes += nRead;
- if (nRead <= 0) break;
- bytesToConsume -= nRead;
- }
- }
- }
- }
- } catch (IOException e) {
- if (DEBUG) Slog.w(TAG, "io exception on restore socket read: " + e.getMessage());
- setResult(RestoreEngine.TRANSPORT_FAILURE);
- info = null;
- }
-
- // If we got here we're either running smoothly or we've finished
- if (info == null) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "No [more] data for this package; tearing down");
- }
- tearDownPipes();
- setRunning(false);
- if (mustKillAgent) {
- tearDownAgent(mTargetApp);
- }
- }
- return (info != null);
- }
-
- void setUpPipes() throws IOException {
- mPipes = ParcelFileDescriptor.createPipe();
- }
-
- void tearDownPipes() {
- // Teardown might arise from the inline restore processing or from the asynchronous
- // timeout mechanism, and these might race. Make sure we don't try to close and
- // null out the pipes twice.
- synchronized (this) {
- if (mPipes != null) {
- try {
- mPipes[0].close();
- mPipes[0] = null;
- mPipes[1].close();
- mPipes[1] = null;
- } catch (IOException e) {
- Slog.w(TAG, "Couldn't close agent pipes", e);
- }
- mPipes = null;
- }
- }
- }
-
- void tearDownAgent(ApplicationInfo app) {
- if (mAgent != null) {
- tearDownAgentAndKill(app);
- mAgent = null;
- }
- }
-
- void handleTimeout() {
- tearDownPipes();
- setResult(RestoreEngine.TARGET_FAILURE);
- setRunning(false);
- }
-
- class RestoreInstallObserver extends PackageInstallObserver {
- final AtomicBoolean mDone = new AtomicBoolean();
- String mPackageName;
- int mResult;
-
- public void reset() {
- synchronized (mDone) {
- mDone.set(false);
- }
- }
-
- public void waitForCompletion() {
- synchronized (mDone) {
- while (mDone.get() == false) {
- try {
- mDone.wait();
- } catch (InterruptedException e) { }
- }
- }
- }
-
- int getResult() {
- return mResult;
- }
-
- @Override
- public void onPackageInstalled(String packageName, int returnCode,
- String msg, Bundle extras) {
- synchronized (mDone) {
- mResult = returnCode;
- mPackageName = packageName;
- mDone.set(true);
- mDone.notifyAll();
- }
- }
- }
-
- class RestoreDeleteObserver extends IPackageDeleteObserver.Stub {
- final AtomicBoolean mDone = new AtomicBoolean();
- int mResult;
-
- public void reset() {
- synchronized (mDone) {
- mDone.set(false);
- }
- }
-
- public void waitForCompletion() {
- synchronized (mDone) {
- while (mDone.get() == false) {
- try {
- mDone.wait();
- } catch (InterruptedException e) { }
- }
- }
- }
-
- @Override
- public void packageDeleted(String packageName, int returnCode) throws RemoteException {
- synchronized (mDone) {
- mResult = returnCode;
- mDone.set(true);
- mDone.notifyAll();
- }
- }
- }
-
- final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver();
- final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver();
-
- boolean installApk(FileMetadata info, String installerPackage, InputStream instream) {
- boolean okay = true;
-
- if (DEBUG) Slog.d(TAG, "Installing from backup: " + info.packageName);
-
- // The file content is an .apk file. Copy it out to a staging location and
- // attempt to install it.
- File apkFile = new File(mDataDir, info.packageName);
- try {
- FileOutputStream apkStream = new FileOutputStream(apkFile);
- byte[] buffer = new byte[32 * 1024];
- long size = info.size;
- while (size > 0) {
- long toRead = (buffer.length < size) ? buffer.length : size;
- int didRead = instream.read(buffer, 0, (int)toRead);
- if (didRead >= 0) mBytes += didRead;
- apkStream.write(buffer, 0, didRead);
- size -= didRead;
- }
- apkStream.close();
-
- // make sure the installer can read it
- apkFile.setReadable(true, false);
-
- // Now install it
- Uri packageUri = Uri.fromFile(apkFile);
- mInstallObserver.reset();
- mPackageManager.installPackage(packageUri, mInstallObserver,
- PackageManager.INSTALL_REPLACE_EXISTING | PackageManager.INSTALL_FROM_ADB,
- installerPackage);
- mInstallObserver.waitForCompletion();
-
- if (mInstallObserver.getResult() != PackageManager.INSTALL_SUCCEEDED) {
- // The only time we continue to accept install of data even if the
- // apk install failed is if we had already determined that we could
- // accept the data regardless.
- if (mPackagePolicies.get(info.packageName) != RestorePolicy.ACCEPT) {
- okay = false;
- }
- } else {
- // Okay, the install succeeded. Make sure it was the right app.
- boolean uninstall = false;
- if (!mInstallObserver.mPackageName.equals(info.packageName)) {
- Slog.w(TAG, "Restore stream claimed to include apk for "
- + info.packageName + " but apk was really "
- + mInstallObserver.mPackageName);
- // delete the package we just put in place; it might be fraudulent
- okay = false;
- uninstall = true;
- } else {
- try {
- PackageInfo pkg = mPackageManager.getPackageInfo(info.packageName,
- PackageManager.GET_SIGNATURES);
- if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) {
- Slog.w(TAG, "Restore stream contains apk of package "
- + info.packageName + " but it disallows backup/restore");
- okay = false;
- } else {
- // So far so good -- do the signatures match the manifest?
- Signature[] sigs = mManifestSignatures.get(info.packageName);
- if (signaturesMatch(sigs, pkg)) {
- // If this is a system-uid app without a declared backup agent,
- // don't restore any of the file data.
- if ((pkg.applicationInfo.uid < Process.FIRST_APPLICATION_UID)
- && (pkg.applicationInfo.backupAgentName == null)) {
- Slog.w(TAG, "Installed app " + info.packageName
- + " has restricted uid and no agent");
- okay = false;
- }
- } else {
- Slog.w(TAG, "Installed app " + info.packageName
- + " signatures do not match restore manifest");
- okay = false;
- uninstall = true;
- }
- }
- } catch (NameNotFoundException e) {
- Slog.w(TAG, "Install of package " + info.packageName
- + " succeeded but now not found");
- okay = false;
- }
- }
-
- // If we're not okay at this point, we need to delete the package
- // that we just installed.
- if (uninstall) {
- mDeleteObserver.reset();
- mPackageManager.deletePackage(mInstallObserver.mPackageName,
- mDeleteObserver, 0);
- mDeleteObserver.waitForCompletion();
- }
- }
- } catch (IOException e) {
- Slog.e(TAG, "Unable to transcribe restored apk for install");
- okay = false;
- } finally {
- apkFile.delete();
- }
-
- return okay;
- }
-
- // Given an actual file content size, consume the post-content padding mandated
- // by the tar format.
- void skipTarPadding(long size, InputStream instream) throws IOException {
- long partial = (size + 512) % 512;
- if (partial > 0) {
- final int needed = 512 - (int)partial;
- if (MORE_DEBUG) {
- Slog.i(TAG, "Skipping tar padding: " + needed + " bytes");
- }
- byte[] buffer = new byte[needed];
- if (readExactly(instream, buffer, 0, needed) == needed) {
- mBytes += needed;
- } else throw new IOException("Unexpected EOF in padding");
- }
- }
-
- // Read a widget metadata file, returning the restored blob
- void readMetadata(FileMetadata info, InputStream instream) throws IOException {
- // Fail on suspiciously large widget dump files
- if (info.size > 64 * 1024) {
- throw new IOException("Metadata too big; corrupt? size=" + info.size);
- }
-
- byte[] buffer = new byte[(int) info.size];
- if (readExactly(instream, buffer, 0, (int)info.size) == info.size) {
- mBytes += info.size;
- } else throw new IOException("Unexpected EOF in widget data");
-
- String[] str = new String[1];
- int offset = extractLine(buffer, 0, str);
- int version = Integer.parseInt(str[0]);
- if (version == BACKUP_MANIFEST_VERSION) {
- offset = extractLine(buffer, offset, str);
- final String pkg = str[0];
- if (info.packageName.equals(pkg)) {
- // Data checks out -- the rest of the buffer is a concatenation of
- // binary blobs as described in the comment at writeAppWidgetData()
- ByteArrayInputStream bin = new ByteArrayInputStream(buffer,
- offset, buffer.length - offset);
- DataInputStream in = new DataInputStream(bin);
- while (bin.available() > 0) {
- int token = in.readInt();
- int size = in.readInt();
- if (size > 64 * 1024) {
- throw new IOException("Datum "
- + Integer.toHexString(token)
- + " too big; corrupt? size=" + info.size);
- }
- switch (token) {
- case BACKUP_WIDGET_METADATA_TOKEN:
- {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Got widget metadata for " + info.packageName);
- }
- mWidgetData = new byte[size];
- in.read(mWidgetData);
- break;
- }
- default:
- {
- if (DEBUG) {
- Slog.i(TAG, "Ignoring metadata blob "
- + Integer.toHexString(token)
- + " for " + info.packageName);
- }
- in.skipBytes(size);
- break;
- }
- }
- }
- } else {
- Slog.w(TAG, "Metadata mismatch: package " + info.packageName
- + " but widget data for " + pkg);
-
- Bundle monitoringExtras = putMonitoringExtra(null,
- EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
- monitoringExtras = putMonitoringExtra(monitoringExtras,
- BackupManagerMonitor.EXTRA_LOG_WIDGET_PACKAGE_NAME, pkg);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_WIDGET_METADATA_MISMATCH,
- null,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- monitoringExtras);
- }
- } else {
- Slog.w(TAG, "Unsupported metadata version " + version);
-
- Bundle monitoringExtras = putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME,
- info.packageName);
- monitoringExtras = putMonitoringExtra(monitoringExtras,
- EXTRA_LOG_EVENT_PACKAGE_VERSION, version);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_WIDGET_UNKNOWN_VERSION,
- null,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- monitoringExtras);
- }
- }
-
- // Returns a policy constant
- RestorePolicy readAppManifest(FileMetadata info, InputStream instream)
- throws IOException {
- // Fail on suspiciously large manifest files
- if (info.size > 64 * 1024) {
- throw new IOException("Restore manifest too big; corrupt? size=" + info.size);
- }
-
- byte[] buffer = new byte[(int) info.size];
- if (MORE_DEBUG) {
- Slog.i(TAG, " readAppManifest() looking for " + info.size + " bytes, "
- + mBytes + " already consumed");
- }
- if (readExactly(instream, buffer, 0, (int)info.size) == info.size) {
- mBytes += info.size;
- } else throw new IOException("Unexpected EOF in manifest");
-
- RestorePolicy policy = RestorePolicy.IGNORE;
- String[] str = new String[1];
- int offset = 0;
-
- try {
- offset = extractLine(buffer, offset, str);
- int version = Integer.parseInt(str[0]);
- if (version == BACKUP_MANIFEST_VERSION) {
- offset = extractLine(buffer, offset, str);
- String manifestPackage = str[0];
- // TODO: handle <original-package>
- if (manifestPackage.equals(info.packageName)) {
- offset = extractLine(buffer, offset, str);
- version = Integer.parseInt(str[0]); // app version
- offset = extractLine(buffer, offset, str);
- // This is the platform version, which we don't use, but we parse it
- // as a safety against corruption in the manifest.
- Integer.parseInt(str[0]);
- offset = extractLine(buffer, offset, str);
- info.installerPackageName = (str[0].length() > 0) ? str[0] : null;
- offset = extractLine(buffer, offset, str);
- boolean hasApk = str[0].equals("1");
- offset = extractLine(buffer, offset, str);
- int numSigs = Integer.parseInt(str[0]);
- if (numSigs > 0) {
- Signature[] sigs = new Signature[numSigs];
- for (int i = 0; i < numSigs; i++) {
- offset = extractLine(buffer, offset, str);
- sigs[i] = new Signature(str[0]);
- }
- mManifestSignatures.put(info.packageName, sigs);
-
- // Okay, got the manifest info we need...
- try {
- PackageInfo pkgInfo = mPackageManager.getPackageInfo(
- info.packageName, PackageManager.GET_SIGNATURES);
- // Fall through to IGNORE if the app explicitly disallows backup
- final int flags = pkgInfo.applicationInfo.flags;
- if ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) {
- // Restore system-uid-space packages only if they have
- // defined a custom backup agent
- if ((pkgInfo.applicationInfo.uid >= Process.FIRST_APPLICATION_UID)
- || (pkgInfo.applicationInfo.backupAgentName != null)) {
- // Verify signatures against any installed version; if they
- // don't match, then we fall though and ignore the data. The
- // signatureMatch() method explicitly ignores the signature
- // check for packages installed on the system partition, because
- // such packages are signed with the platform cert instead of
- // the app developer's cert, so they're different on every
- // device.
- if (signaturesMatch(sigs, pkgInfo)) {
- if ((pkgInfo.applicationInfo.flags
- & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) != 0) {
- Slog.i(TAG, "Package has restoreAnyVersion; taking data");
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_RESTORE_ANY_VERSION,
- pkgInfo,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- policy = RestorePolicy.ACCEPT;
- } else if (pkgInfo.versionCode >= version) {
- Slog.i(TAG, "Sig + version match; taking data");
- policy = RestorePolicy.ACCEPT;
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_VERSIONS_MATCH,
- pkgInfo,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- } else {
- // The data is from a newer version of the app than
- // is presently installed. That means we can only
- // use it if the matching apk is also supplied.
- if (mAllowApks) {
- Slog.i(TAG, "Data version " + version
- + " is newer than installed version "
- + pkgInfo.versionCode
- + " - requiring apk");
- policy = RestorePolicy.ACCEPT_IF_APK;
- } else {
- Slog.i(TAG, "Data requires newer version "
- + version + "; ignoring");
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER,
- pkgInfo,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- putMonitoringExtra(null,
- EXTRA_LOG_OLD_VERSION,
- version));
-
- policy = RestorePolicy.IGNORE;
- }
- }
- } else {
- Slog.w(TAG, "Restore manifest signatures do not match "
- + "installed application for " + info.packageName);
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH,
- pkgInfo,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- }
- } else {
- Slog.w(TAG, "Package " + info.packageName
- + " is system level with no agent");
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_SYSTEM_APP_NO_AGENT,
- pkgInfo,
- LOG_EVENT_CATEGORY_AGENT,
- null);
- }
- } else {
- if (DEBUG) Slog.i(TAG, "Restore manifest from "
- + info.packageName + " but allowBackup=false");
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE,
- pkgInfo,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- }
- } catch (NameNotFoundException e) {
- // Okay, the target app isn't installed. We can process
- // the restore properly only if the dataset provides the
- // apk file and we can successfully install it.
- if (mAllowApks) {
- if (DEBUG) Slog.i(TAG, "Package " + info.packageName
- + " not installed; requiring apk in dataset");
- policy = RestorePolicy.ACCEPT_IF_APK;
- } else {
- policy = RestorePolicy.IGNORE;
- }
- Bundle monitoringExtras = putMonitoringExtra(null,
- EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
- monitoringExtras = putMonitoringExtra(monitoringExtras,
- EXTRA_LOG_POLICY_ALLOW_APKS, mAllowApks);
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_APK_NOT_INSTALLED,
- null,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- monitoringExtras);
- }
-
- if (policy == RestorePolicy.ACCEPT_IF_APK && !hasApk) {
- Slog.i(TAG, "Cannot restore package " + info.packageName
- + " without the matching .apk");
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK,
- null,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- putMonitoringExtra(null,
- EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName));
- }
- } else {
- Slog.i(TAG, "Missing signature on backed-up package "
- + info.packageName);
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_MISSING_SIGNATURE,
- null,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- putMonitoringExtra(null,
- EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName));
- }
- } else {
- Slog.i(TAG, "Expected package " + info.packageName
- + " but restore manifest claims " + manifestPackage);
- Bundle monitoringExtras = putMonitoringExtra(null,
- EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
- monitoringExtras = putMonitoringExtra(monitoringExtras,
- EXTRA_LOG_MANIFEST_PACKAGE_NAME, manifestPackage);
- mMonitor = monitorEvent(mMonitor,
- LOG_EVENT_ID_EXPECTED_DIFFERENT_PACKAGE,
- null,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- monitoringExtras);
- }
- } else {
- Slog.i(TAG, "Unknown restore manifest version " + version
- + " for package " + info.packageName);
- Bundle monitoringExtras = putMonitoringExtra(null,
- EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName);
- monitoringExtras = putMonitoringExtra(monitoringExtras,
- EXTRA_LOG_EVENT_PACKAGE_VERSION, version);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_UNKNOWN_VERSION,
- null,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- monitoringExtras);
-
- }
- } catch (NumberFormatException e) {
- Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_CORRUPT_MANIFEST,
- null,
- LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName));
- } catch (IllegalArgumentException e) {
- Slog.w(TAG, e.getMessage());
- }
-
- return policy;
- }
-
- // Builds a line from a byte buffer starting at 'offset', and returns
- // the index of the next unconsumed data in the buffer.
- int extractLine(byte[] buffer, int offset, String[] outStr) throws IOException {
- final int end = buffer.length;
- if (offset >= end) throw new IOException("Incomplete data");
-
- int pos;
- for (pos = offset; pos < end; pos++) {
- byte c = buffer[pos];
- // at LF we declare end of line, and return the next char as the
- // starting point for the next time through
- if (c == '\n') {
- break;
- }
- }
- outStr[0] = new String(buffer, offset, pos - offset);
- pos++; // may be pointing an extra byte past the end but that's okay
- return pos;
- }
-
- void dumpFileMetadata(FileMetadata info) {
- if (MORE_DEBUG) {
- StringBuilder b = new StringBuilder(128);
-
- // mode string
- b.append((info.type == BackupAgent.TYPE_DIRECTORY) ? 'd' : '-');
- b.append(((info.mode & 0400) != 0) ? 'r' : '-');
- b.append(((info.mode & 0200) != 0) ? 'w' : '-');
- b.append(((info.mode & 0100) != 0) ? 'x' : '-');
- b.append(((info.mode & 0040) != 0) ? 'r' : '-');
- b.append(((info.mode & 0020) != 0) ? 'w' : '-');
- b.append(((info.mode & 0010) != 0) ? 'x' : '-');
- b.append(((info.mode & 0004) != 0) ? 'r' : '-');
- b.append(((info.mode & 0002) != 0) ? 'w' : '-');
- b.append(((info.mode & 0001) != 0) ? 'x' : '-');
- b.append(String.format(" %9d ", info.size));
-
- Date stamp = new Date(info.mtime);
- b.append(new SimpleDateFormat("MMM dd HH:mm:ss ").format(stamp));
-
- b.append(info.packageName);
- b.append(" :: ");
- b.append(info.domain);
- b.append(" :: ");
- b.append(info.path);
-
- Slog.i(TAG, b.toString());
- }
- }
-
- // Consume a tar file header block [sequence] and accumulate the relevant metadata
- FileMetadata readTarHeaders(InputStream instream) throws IOException {
- byte[] block = new byte[512];
- FileMetadata info = null;
-
- boolean gotHeader = readTarHeader(instream, block);
- if (gotHeader) {
- try {
- // okay, presume we're okay, and extract the various metadata
- info = new FileMetadata();
- info.size = extractRadix(block, TAR_HEADER_OFFSET_FILESIZE,
- TAR_HEADER_LENGTH_FILESIZE, TAR_HEADER_LONG_RADIX);
- info.mtime = extractRadix(block, TAR_HEADER_OFFSET_MODTIME,
- TAR_HEADER_LENGTH_MODTIME, TAR_HEADER_LONG_RADIX);
- info.mode = extractRadix(block, TAR_HEADER_OFFSET_MODE,
- TAR_HEADER_LENGTH_MODE, TAR_HEADER_LONG_RADIX);
-
- info.path = extractString(block, TAR_HEADER_OFFSET_PATH_PREFIX,
- TAR_HEADER_LENGTH_PATH_PREFIX);
- String path = extractString(block, TAR_HEADER_OFFSET_PATH,
- TAR_HEADER_LENGTH_PATH);
- if (path.length() > 0) {
- if (info.path.length() > 0) info.path += '/';
- info.path += path;
- }
-
- // tar link indicator field: 1 byte at offset 156 in the header.
- int typeChar = block[TAR_HEADER_OFFSET_TYPE_CHAR];
- if (typeChar == 'x') {
- // pax extended header, so we need to read that
- gotHeader = readPaxExtendedHeader(instream, info);
- if (gotHeader) {
- // and after a pax extended header comes another real header -- read
- // that to find the real file type
- gotHeader = readTarHeader(instream, block);
- }
- if (!gotHeader) throw new IOException("Bad or missing pax header");
-
- typeChar = block[TAR_HEADER_OFFSET_TYPE_CHAR];
- }
-
- switch (typeChar) {
- case '0': info.type = BackupAgent.TYPE_FILE; break;
- case '5': {
- info.type = BackupAgent.TYPE_DIRECTORY;
- if (info.size != 0) {
- Slog.w(TAG, "Directory entry with nonzero size in header");
- info.size = 0;
- }
- break;
- }
- case 0: {
- // presume EOF
- if (MORE_DEBUG) Slog.w(TAG, "Saw type=0 in tar header block, info=" + info);
- return null;
- }
- default: {
- Slog.e(TAG, "Unknown tar entity type: " + typeChar);
- throw new IOException("Unknown entity type " + typeChar);
- }
- }
-
- // Parse out the path
- //
- // first: apps/shared/unrecognized
- if (FullBackup.SHARED_PREFIX.regionMatches(0,
- info.path, 0, FullBackup.SHARED_PREFIX.length())) {
- // File in shared storage. !!! TODO: implement this.
- info.path = info.path.substring(FullBackup.SHARED_PREFIX.length());
- info.packageName = SHARED_BACKUP_AGENT_PACKAGE;
- info.domain = FullBackup.SHARED_STORAGE_TOKEN;
- if (DEBUG) Slog.i(TAG, "File in shared storage: " + info.path);
- } else if (FullBackup.APPS_PREFIX.regionMatches(0,
- info.path, 0, FullBackup.APPS_PREFIX.length())) {
- // App content! Parse out the package name and domain
-
- // strip the apps/ prefix
- info.path = info.path.substring(FullBackup.APPS_PREFIX.length());
-
- // extract the package name
- int slash = info.path.indexOf('/');
- if (slash < 0) throw new IOException("Illegal semantic path in " + info.path);
- info.packageName = info.path.substring(0, slash);
- info.path = info.path.substring(slash+1);
-
- // if it's a manifest or metadata payload we're done, otherwise parse
- // out the domain into which the file will be restored
- if (!info.path.equals(BACKUP_MANIFEST_FILENAME)
- && !info.path.equals(BACKUP_METADATA_FILENAME)) {
- slash = info.path.indexOf('/');
- if (slash < 0) {
- throw new IOException("Illegal semantic path in non-manifest "
- + info.path);
- }
- info.domain = info.path.substring(0, slash);
- info.path = info.path.substring(slash + 1);
- }
- }
- } catch (IOException e) {
- if (DEBUG) {
- Slog.e(TAG, "Parse error in header: " + e.getMessage());
- if (MORE_DEBUG) {
- HEXLOG(block);
- }
- }
- throw e;
- }
- }
- return info;
- }
-
- private boolean isRestorableFile(FileMetadata info) {
- if (FullBackup.CACHE_TREE_TOKEN.equals(info.domain)) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Dropping cache file path " + info.path);
- }
- return false;
- }
-
- if (FullBackup.ROOT_TREE_TOKEN.equals(info.domain)) {
- // It's possible this is "no-backup" dir contents in an archive stream
- // produced on a device running a version of the OS that predates that
- // API. Respect the no-backup intention and don't let the data get to
- // the app.
- if (info.path.startsWith("no_backup/")) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Dropping no_backup file path " + info.path);
- }
- return false;
- }
- }
-
- // The path needs to be canonical
- if (info.path.contains("..") || info.path.contains("//")) {
- if (MORE_DEBUG) {
- Slog.w(TAG, "Dropping invalid path " + info.path);
- }
- return false;
- }
-
- // Otherwise we think this file is good to go
- return true;
- }
-
- private void HEXLOG(byte[] block) {
- int offset = 0;
- int todo = block.length;
- StringBuilder buf = new StringBuilder(64);
- while (todo > 0) {
- buf.append(String.format("%04x ", offset));
- int numThisLine = (todo > 16) ? 16 : todo;
- for (int i = 0; i < numThisLine; i++) {
- buf.append(String.format("%02x ", block[offset+i]));
- }
- Slog.i("hexdump", buf.toString());
- buf.setLength(0);
- todo -= numThisLine;
- offset += numThisLine;
- }
- }
-
- // Read exactly the given number of bytes into a buffer at the stated offset.
- // Returns false if EOF is encountered before the requested number of bytes
- // could be read.
- int readExactly(InputStream in, byte[] buffer, int offset, int size)
- throws IOException {
- if (size <= 0) throw new IllegalArgumentException("size must be > 0");
-if (MORE_DEBUG) Slog.i(TAG, " ... readExactly(" + size + ") called");
- int soFar = 0;
- while (soFar < size) {
- int nRead = in.read(buffer, offset + soFar, size - soFar);
- if (nRead <= 0) {
- if (MORE_DEBUG) Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar);
- break;
- }
- soFar += nRead;
-if (MORE_DEBUG) Slog.v(TAG, " + got " + nRead + "; now wanting " + (size - soFar));
- }
- return soFar;
- }
-
- boolean readTarHeader(InputStream instream, byte[] block) throws IOException {
- final int got = readExactly(instream, block, 0, 512);
- if (got == 0) return false; // Clean EOF
- if (got < 512) throw new IOException("Unable to read full block header");
- mBytes += 512;
- return true;
- }
-
- // overwrites 'info' fields based on the pax extended header
- boolean readPaxExtendedHeader(InputStream instream, FileMetadata info)
- throws IOException {
- // We should never see a pax extended header larger than this
- if (info.size > 32*1024) {
- Slog.w(TAG, "Suspiciously large pax header size " + info.size
- + " - aborting");
- throw new IOException("Sanity failure: pax header size " + info.size);
- }
-
- // read whole blocks, not just the content size
- int numBlocks = (int)((info.size + 511) >> 9);
- byte[] data = new byte[numBlocks * 512];
- if (readExactly(instream, data, 0, data.length) < data.length) {
- throw new IOException("Unable to read full pax header");
- }
- mBytes += data.length;
-
- final int contentSize = (int) info.size;
- int offset = 0;
- do {
- // extract the line at 'offset'
- int eol = offset+1;
- while (eol < contentSize && data[eol] != ' ') eol++;
- if (eol >= contentSize) {
- // error: we just hit EOD looking for the end of the size field
- throw new IOException("Invalid pax data");
- }
- // eol points to the space between the count and the key
- int linelen = (int) extractRadix(data, offset, eol - offset, 10);
- int key = eol + 1; // start of key=value
- eol = offset + linelen - 1; // trailing LF
- int value;
- for (value = key+1; data[value] != '=' && value <= eol; value++);
- if (value > eol) {
- throw new IOException("Invalid pax declaration");
- }
-
- // pax requires that key/value strings be in UTF-8
- String keyStr = new String(data, key, value-key, "UTF-8");
- // -1 to strip the trailing LF
- String valStr = new String(data, value+1, eol-value-1, "UTF-8");
-
- if ("path".equals(keyStr)) {
- info.path = valStr;
- } else if ("size".equals(keyStr)) {
- info.size = Long.parseLong(valStr);
- } else {
- if (DEBUG) Slog.i(TAG, "Unhandled pax key: " + key);
- }
-
- offset += linelen;
- } while (offset < contentSize);
-
- return true;
- }
-
- long extractRadix(byte[] data, int offset, int maxChars, int radix)
- throws IOException {
- long value = 0;
- final int end = offset + maxChars;
- for (int i = offset; i < end; i++) {
- final byte b = data[i];
- // Numeric fields in tar can terminate with either NUL or SPC
- if (b == 0 || b == ' ') break;
- if (b < '0' || b > ('0' + radix - 1)) {
- throw new IOException("Invalid number in header: '" + (char)b
- + "' for radix " + radix);
- }
- value = radix * value + (b - '0');
- }
- return value;
- }
-
- String extractString(byte[] data, int offset, int maxChars) throws IOException {
- final int end = offset + maxChars;
- int eos = offset;
- // tar string fields terminate early with a NUL
- while (eos < end && data[eos] != 0) eos++;
- return new String(data, offset, eos-offset, "US-ASCII");
- }
-
- void sendStartRestore() {
- if (mObserver != null) {
- try {
- mObserver.onStartRestore();
- } catch (RemoteException e) {
- Slog.w(TAG, "full restore observer went away: startRestore");
- mObserver = null;
- }
- }
- }
-
- void sendOnRestorePackage(String name) {
- if (mObserver != null) {
- try {
- // TODO: use a more user-friendly name string
- mObserver.onRestorePackage(name);
- } catch (RemoteException e) {
- Slog.w(TAG, "full restore observer went away: restorePackage");
- mObserver = null;
- }
- }
- }
-
- void sendEndRestore() {
- if (mObserver != null) {
- try {
- mObserver.onEndRestore();
- } catch (RemoteException e) {
- Slog.w(TAG, "full restore observer went away: endRestore");
- mObserver = null;
- }
- }
- }
- }
-
- // ***** end new engine class ***
-
- // Used for synchronizing doRestoreFinished during adb restore
- class AdbRestoreFinishedLatch implements BackupRestoreTask {
- static final String TAG = "AdbRestoreFinishedLatch";
- final CountDownLatch mLatch;
- private final int mCurrentOpToken;
-
- AdbRestoreFinishedLatch(int currentOpToken) {
- mLatch = new CountDownLatch(1);
- mCurrentOpToken = currentOpToken;
- }
-
- void await() {
- boolean latched = false;
- try {
- latched = mLatch.await(TIMEOUT_FULL_BACKUP_INTERVAL, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- Slog.w(TAG, "Interrupted!");
- }
- }
-
- @Override
- public void execute() {
- // Unused
- }
-
- @Override
- public void operationComplete(long result) {
- if (MORE_DEBUG) {
- Slog.w(TAG, "adb onRestoreFinished() complete");
- }
- mLatch.countDown();
- removeOperation(mCurrentOpToken);
- }
-
- @Override
- public void handleCancel(boolean cancelAll) {
- if (DEBUG) {
- Slog.w(TAG, "adb onRestoreFinished() timed out");
- }
- mLatch.countDown();
- removeOperation(mCurrentOpToken);
- }
- }
-
- class PerformAdbRestoreTask implements Runnable {
- ParcelFileDescriptor mInputFile;
- String mCurrentPassword;
- String mDecryptPassword;
- IFullBackupRestoreObserver mObserver;
- AtomicBoolean mLatchObject;
- IBackupAgent mAgent;
- PackageManagerBackupAgent mPackageManagerBackupAgent;
- String mAgentPackage;
- ApplicationInfo mTargetApp;
- FullBackupObbConnection mObbConnection = null;
- ParcelFileDescriptor[] mPipes = null;
- byte[] mWidgetData = null;
-
- long mBytes;
-
- // Runner that can be placed on a separate thread to do in-process invocation
- // of the "restore finished" API asynchronously. Used by adb restore.
- class RestoreFinishedRunnable implements Runnable {
- final IBackupAgent mAgent;
- final int mToken;
-
- RestoreFinishedRunnable(IBackupAgent agent, int token) {
- mAgent = agent;
- mToken = token;
- }
-
- @Override
- public void run() {
- try {
- mAgent.doRestoreFinished(mToken, mBackupManagerBinder);
- } catch (RemoteException e) {
- // never happens; this is used only for local binder calls
- }
- }
- }
-
- // possible handling states for a given package in the restore dataset
- final HashMap<String, RestorePolicy> mPackagePolicies
- = new HashMap<String, RestorePolicy>();
-
- // installer package names for each encountered app, derived from the manifests
- final HashMap<String, String> mPackageInstallers = new HashMap<String, String>();
-
- // Signatures for a given package found in its manifest file
- final HashMap<String, Signature[]> mManifestSignatures
- = new HashMap<String, Signature[]>();
-
- // Packages we've already wiped data on when restoring their first file
- final HashSet<String> mClearedPackages = new HashSet<String>();
-
- PerformAdbRestoreTask(ParcelFileDescriptor fd, String curPassword, String decryptPassword,
- IFullBackupRestoreObserver observer, AtomicBoolean latch) {
- mInputFile = fd;
- mCurrentPassword = curPassword;
- mDecryptPassword = decryptPassword;
- mObserver = observer;
- mLatchObject = latch;
- mAgent = null;
- mPackageManagerBackupAgent = makeMetadataAgent();
- mAgentPackage = null;
- mTargetApp = null;
- mObbConnection = new FullBackupObbConnection();
-
- // Which packages we've already wiped data on. We prepopulate this
- // with a whitelist of packages known to be unclearable.
- mClearedPackages.add("android");
- mClearedPackages.add(SETTINGS_PACKAGE);
- }
-
- class RestoreFileRunnable implements Runnable {
- IBackupAgent mAgent;
- FileMetadata mInfo;
- ParcelFileDescriptor mSocket;
- int mToken;
-
- RestoreFileRunnable(IBackupAgent agent, FileMetadata info,
- ParcelFileDescriptor socket, int token) throws IOException {
- mAgent = agent;
- mInfo = info;
- mToken = token;
-
- // This class is used strictly for process-local binder invocations. The
- // semantics of ParcelFileDescriptor differ in this case; in particular, we
- // do not automatically get a 'dup'ed descriptor that we can can continue
- // to use asynchronously from the caller. So, we make sure to dup it ourselves
- // before proceeding to do the restore.
- mSocket = ParcelFileDescriptor.dup(socket.getFileDescriptor());
- }
-
- @Override
- public void run() {
- try {
- mAgent.doRestoreFile(mSocket, mInfo.size, mInfo.type,
- mInfo.domain, mInfo.path, mInfo.mode, mInfo.mtime,
- mToken, mBackupManagerBinder);
- } catch (RemoteException e) {
- // never happens; this is used strictly for local binder calls
- }
- }
- }
-
- @Override
- public void run() {
- Slog.i(TAG, "--- Performing full-dataset restore ---");
- mObbConnection.establish();
- sendStartRestore();
-
- // Are we able to restore shared-storage data?
- if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- mPackagePolicies.put(SHARED_BACKUP_AGENT_PACKAGE, RestorePolicy.ACCEPT);
- }
-
- FileInputStream rawInStream = null;
- DataInputStream rawDataIn = null;
- try {
- if (!backupPasswordMatches(mCurrentPassword)) {
- if (DEBUG) Slog.w(TAG, "Backup password mismatch; aborting");
- return;
- }
-
- mBytes = 0;
- byte[] buffer = new byte[32 * 1024];
- rawInStream = new FileInputStream(mInputFile.getFileDescriptor());
- rawDataIn = new DataInputStream(rawInStream);
-
- // First, parse out the unencrypted/uncompressed header
- boolean compressed = false;
- InputStream preCompressStream = rawInStream;
- final InputStream in;
-
- boolean okay = false;
- final int headerLen = BACKUP_FILE_HEADER_MAGIC.length();
- byte[] streamHeader = new byte[headerLen];
- rawDataIn.readFully(streamHeader);
- byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes("UTF-8");
- if (Arrays.equals(magicBytes, streamHeader)) {
- // okay, header looks good. now parse out the rest of the fields.
- String s = readHeaderLine(rawInStream);
- final int archiveVersion = Integer.parseInt(s);
- if (archiveVersion <= BACKUP_FILE_VERSION) {
- // okay, it's a version we recognize. if it's version 1, we may need
- // to try two different PBKDF2 regimes to compare checksums.
- final boolean pbkdf2Fallback = (archiveVersion == 1);
-
- s = readHeaderLine(rawInStream);
- compressed = (Integer.parseInt(s) != 0);
- s = readHeaderLine(rawInStream);
- if (s.equals("none")) {
- // no more header to parse; we're good to go
- okay = true;
- } else if (mDecryptPassword != null && mDecryptPassword.length() > 0) {
- preCompressStream = decodeAesHeaderAndInitialize(s, pbkdf2Fallback,
- rawInStream);
- if (preCompressStream != null) {
- okay = true;
- }
- } else Slog.w(TAG, "Archive is encrypted but no password given");
- } else Slog.w(TAG, "Wrong header version: " + s);
- } else Slog.w(TAG, "Didn't read the right header magic");
-
- if (!okay) {
- Slog.w(TAG, "Invalid restore data; aborting.");
- return;
- }
-
- // okay, use the right stream layer based on compression
- in = (compressed) ? new InflaterInputStream(preCompressStream) : preCompressStream;
-
- boolean didRestore;
- do {
- didRestore = restoreOneFile(in, buffer);
- } while (didRestore);
-
- if (MORE_DEBUG) Slog.v(TAG, "Done consuming input tarfile, total bytes=" + mBytes);
- } catch (IOException e) {
- Slog.e(TAG, "Unable to read restore input");
- } finally {
- tearDownPipes();
- tearDownAgent(mTargetApp, true);
-
- try {
- if (rawDataIn != null) rawDataIn.close();
- if (rawInStream != null) rawInStream.close();
- mInputFile.close();
- } catch (IOException e) {
- Slog.w(TAG, "Close of restore data pipe threw", e);
- /* nothing we can do about this */
- }
- synchronized (mLatchObject) {
- mLatchObject.set(true);
- mLatchObject.notifyAll();
- }
- mObbConnection.tearDown();
- sendEndRestore();
- Slog.d(TAG, "Full restore pass complete.");
- mWakelock.release();
- }
- }
-
- String readHeaderLine(InputStream in) throws IOException {
- int c;
- StringBuilder buffer = new StringBuilder(80);
- while ((c = in.read()) >= 0) {
- if (c == '\n') break; // consume and discard the newlines
- buffer.append((char)c);
- }
- return buffer.toString();
- }
-
- InputStream attemptMasterKeyDecryption(String algorithm, byte[] userSalt, byte[] ckSalt,
- int rounds, String userIvHex, String masterKeyBlobHex, InputStream rawInStream,
- boolean doLog) {
- InputStream result = null;
-
- try {
- Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
- SecretKey userKey = buildPasswordKey(algorithm, mDecryptPassword, userSalt,
- rounds);
- byte[] IV = hexToByteArray(userIvHex);
- IvParameterSpec ivSpec = new IvParameterSpec(IV);
- c.init(Cipher.DECRYPT_MODE,
- new SecretKeySpec(userKey.getEncoded(), "AES"),
- ivSpec);
- byte[] mkCipher = hexToByteArray(masterKeyBlobHex);
- byte[] mkBlob = c.doFinal(mkCipher);
-
- // first, the master key IV
- int offset = 0;
- int len = mkBlob[offset++];
- IV = Arrays.copyOfRange(mkBlob, offset, offset + len);
- offset += len;
- // then the master key itself
- len = mkBlob[offset++];
- byte[] mk = Arrays.copyOfRange(mkBlob,
- offset, offset + len);
- offset += len;
- // and finally the master key checksum hash
- len = mkBlob[offset++];
- byte[] mkChecksum = Arrays.copyOfRange(mkBlob,
- offset, offset + len);
-
- // now validate the decrypted master key against the checksum
- byte[] calculatedCk = makeKeyChecksum(algorithm, mk, ckSalt, rounds);
- if (Arrays.equals(calculatedCk, mkChecksum)) {
- ivSpec = new IvParameterSpec(IV);
- c.init(Cipher.DECRYPT_MODE,
- new SecretKeySpec(mk, "AES"),
- ivSpec);
- // Only if all of the above worked properly will 'result' be assigned
- result = new CipherInputStream(rawInStream, c);
- } else if (doLog) Slog.w(TAG, "Incorrect password");
- } catch (InvalidAlgorithmParameterException e) {
- if (doLog) Slog.e(TAG, "Needed parameter spec unavailable!", e);
- } catch (BadPaddingException e) {
- // This case frequently occurs when the wrong password is used to decrypt
- // the master key. Use the identical "incorrect password" log text as is
- // used in the checksum failure log in order to avoid providing additional
- // information to an attacker.
- if (doLog) Slog.w(TAG, "Incorrect password");
- } catch (IllegalBlockSizeException e) {
- if (doLog) Slog.w(TAG, "Invalid block size in master key");
- } catch (NoSuchAlgorithmException e) {
- if (doLog) Slog.e(TAG, "Needed decryption algorithm unavailable!");
- } catch (NoSuchPaddingException e) {
- if (doLog) Slog.e(TAG, "Needed padding mechanism unavailable!");
- } catch (InvalidKeyException e) {
- if (doLog) Slog.w(TAG, "Illegal password; aborting");
- }
-
- return result;
- }
-
- InputStream decodeAesHeaderAndInitialize(String encryptionName, boolean pbkdf2Fallback,
- InputStream rawInStream) {
- InputStream result = null;
- try {
- if (encryptionName.equals(ENCRYPTION_ALGORITHM_NAME)) {
-
- String userSaltHex = readHeaderLine(rawInStream); // 5
- byte[] userSalt = hexToByteArray(userSaltHex);
-
- String ckSaltHex = readHeaderLine(rawInStream); // 6
- byte[] ckSalt = hexToByteArray(ckSaltHex);
-
- int rounds = Integer.parseInt(readHeaderLine(rawInStream)); // 7
- String userIvHex = readHeaderLine(rawInStream); // 8
-
- String masterKeyBlobHex = readHeaderLine(rawInStream); // 9
-
- // decrypt the master key blob
- result = attemptMasterKeyDecryption(PBKDF_CURRENT, userSalt, ckSalt,
- rounds, userIvHex, masterKeyBlobHex, rawInStream, false);
- if (result == null && pbkdf2Fallback) {
- result = attemptMasterKeyDecryption(PBKDF_FALLBACK, userSalt, ckSalt,
- rounds, userIvHex, masterKeyBlobHex, rawInStream, true);
- }
- } else Slog.w(TAG, "Unsupported encryption method: " + encryptionName);
- } catch (NumberFormatException e) {
- Slog.w(TAG, "Can't parse restore data header");
- } catch (IOException e) {
- Slog.w(TAG, "Can't read input header");
- }
-
- return result;
- }
-
- boolean restoreOneFile(InputStream instream, byte[] buffer) {
- FileMetadata info;
- try {
- info = readTarHeaders(instream);
- if (info != null) {
- if (MORE_DEBUG) {
- dumpFileMetadata(info);
- }
-
- final String pkg = info.packageName;
- if (!pkg.equals(mAgentPackage)) {
- // okay, change in package; set up our various
- // bookkeeping if we haven't seen it yet
- if (!mPackagePolicies.containsKey(pkg)) {
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
- }
-
- // Clean up the previous agent relationship if necessary,
- // and let the observer know we're considering a new app.
- if (mAgent != null) {
- if (DEBUG) Slog.d(TAG, "Saw new package; finalizing old one");
- // Now we're really done
- tearDownPipes();
- tearDownAgent(mTargetApp, true);
- mTargetApp = null;
- mAgentPackage = null;
- }
- }
-
- if (info.path.equals(BACKUP_MANIFEST_FILENAME)) {
- mPackagePolicies.put(pkg, readAppManifest(info, instream));
- mPackageInstallers.put(pkg, info.installerPackageName);
- // We've read only the manifest content itself at this point,
- // so consume the footer before looping around to the next
- // input file
- skipTarPadding(info.size, instream);
- sendOnRestorePackage(pkg);
- } else if (info.path.equals(BACKUP_METADATA_FILENAME)) {
- // Metadata blobs!
- readMetadata(info, instream);
- skipTarPadding(info.size, instream);
- } else {
- // Non-manifest, so it's actual file data. Is this a package
- // we're ignoring?
- boolean okay = true;
- RestorePolicy policy = mPackagePolicies.get(pkg);
- switch (policy) {
- case IGNORE:
- okay = false;
- break;
-
- case ACCEPT_IF_APK:
- // If we're in accept-if-apk state, then the first file we
- // see MUST be the apk.
- if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) {
- if (DEBUG) Slog.d(TAG, "APK file; installing");
- // Try to install the app.
- String installerName = mPackageInstallers.get(pkg);
- okay = installApk(info, installerName, instream);
- // good to go; promote to ACCEPT
- mPackagePolicies.put(pkg, (okay)
- ? RestorePolicy.ACCEPT
- : RestorePolicy.IGNORE);
- // At this point we've consumed this file entry
- // ourselves, so just strip the tar footer and
- // go on to the next file in the input stream
- skipTarPadding(info.size, instream);
- return true;
- } else {
- // File data before (or without) the apk. We can't
- // handle it coherently in this case so ignore it.
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
- okay = false;
- }
- break;
-
- case ACCEPT:
- if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) {
- if (DEBUG) Slog.d(TAG, "apk present but ACCEPT");
- // we can take the data without the apk, so we
- // *want* to do so. skip the apk by declaring this
- // one file not-okay without changing the restore
- // policy for the package.
- okay = false;
- }
- break;
-
- default:
- // Something has gone dreadfully wrong when determining
- // the restore policy from the manifest. Ignore the
- // rest of this package's data.
- Slog.e(TAG, "Invalid policy from manifest");
- okay = false;
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
- break;
- }
-
- // The path needs to be canonical
- if (info.path.contains("..") || info.path.contains("//")) {
- if (MORE_DEBUG) {
- Slog.w(TAG, "Dropping invalid path " + info.path);
- }
- okay = false;
- }
-
- // If the policy is satisfied, go ahead and set up to pipe the
- // data to the agent.
- if (DEBUG && okay && mAgent != null) {
- Slog.i(TAG, "Reusing existing agent instance");
- }
- if (okay && mAgent == null) {
- if (DEBUG) Slog.d(TAG, "Need to launch agent for " + pkg);
-
- try {
- mTargetApp = mPackageManager.getApplicationInfo(pkg, 0);
-
- // If we haven't sent any data to this app yet, we probably
- // need to clear it first. Check that.
- if (!mClearedPackages.contains(pkg)) {
- // apps with their own backup agents are
- // responsible for coherently managing a full
- // restore.
- if (mTargetApp.backupAgentName == null) {
- if (DEBUG) Slog.d(TAG, "Clearing app data preparatory to full restore");
- clearApplicationDataSynchronous(pkg);
- } else {
- if (DEBUG) Slog.d(TAG, "backup agent ("
- + mTargetApp.backupAgentName + ") => no clear");
- }
- mClearedPackages.add(pkg);
- } else {
- if (DEBUG) Slog.d(TAG, "We've initialized this app already; no clear required");
- }
-
- // All set; now set up the IPC and launch the agent
- setUpPipes();
- mAgent = bindToAgentSynchronous(mTargetApp,
- FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain)
- ? ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL
- : ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL);
- mAgentPackage = pkg;
- } catch (IOException e) {
- // fall through to error handling
- } catch (NameNotFoundException e) {
- // fall through to error handling
- }
-
- if (mAgent == null) {
- if (DEBUG) Slog.d(TAG, "Unable to create agent for " + pkg);
- okay = false;
- tearDownPipes();
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
- }
- }
-
- // Sanity check: make sure we never give data to the wrong app. This
- // should never happen but a little paranoia here won't go amiss.
- if (okay && !pkg.equals(mAgentPackage)) {
- Slog.e(TAG, "Restoring data for " + pkg
- + " but agent is for " + mAgentPackage);
- okay = false;
- }
-
- // At this point we have an agent ready to handle the full
- // restore data as well as a pipe for sending data to
- // that agent. Tell the agent to start reading from the
- // pipe.
- if (okay) {
- boolean agentSuccess = true;
- long toCopy = info.size;
- final boolean isSharedStorage = pkg.equals(SHARED_BACKUP_AGENT_PACKAGE);
- final long timeout = isSharedStorage ?
- TIMEOUT_SHARED_BACKUP_INTERVAL : TIMEOUT_RESTORE_INTERVAL;
- final int token = generateRandomIntegerToken();
- try {
- prepareOperationTimeout(token, timeout, null,
- OP_TYPE_RESTORE_WAIT);
- if (FullBackup.OBB_TREE_TOKEN.equals(info.domain)) {
- if (DEBUG) Slog.d(TAG, "Restoring OBB file for " + pkg
- + " : " + info.path);
- mObbConnection.restoreObbFile(pkg, mPipes[0],
- info.size, info.type, info.path, info.mode,
- info.mtime, token, mBackupManagerBinder);
- } else if (FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain)) {
- if (DEBUG) Slog.d(TAG, "Restoring key-value file for " + pkg
- + " : " + info.path);
- KeyValueAdbRestoreEngine restoreEngine =
- new KeyValueAdbRestoreEngine(BackupManagerService.this,
- mDataDir, info, mPipes[0], mAgent, token);
- new Thread(restoreEngine, "restore-key-value-runner").start();
- } else {
- if (DEBUG) Slog.d(TAG, "Invoking agent to restore file "
- + info.path);
- // fire up the app's agent listening on the socket. If
- // the agent is running in the system process we can't
- // just invoke it asynchronously, so we provide a thread
- // for it here.
- if (mTargetApp.processName.equals("system")) {
- Slog.d(TAG, "system process agent - spinning a thread");
- RestoreFileRunnable runner = new RestoreFileRunnable(
- mAgent, info, mPipes[0], token);
- new Thread(runner, "restore-sys-runner").start();
- } else {
- mAgent.doRestoreFile(mPipes[0], info.size, info.type,
- info.domain, info.path, info.mode, info.mtime,
- token, mBackupManagerBinder);
- }
- }
- } catch (IOException e) {
- // couldn't dup the socket for a process-local restore
- Slog.d(TAG, "Couldn't establish restore");
- agentSuccess = false;
- okay = false;
- } catch (RemoteException e) {
- // whoops, remote entity went away. We'll eat the content
- // ourselves, then, and not copy it over.
- Slog.e(TAG, "Agent crashed during full restore");
- agentSuccess = false;
- okay = false;
- }
-
- // Copy over the data if the agent is still good
- if (okay) {
- boolean pipeOkay = true;
- FileOutputStream pipe = new FileOutputStream(
- mPipes[1].getFileDescriptor());
- while (toCopy > 0) {
- int toRead = (toCopy > buffer.length)
- ? buffer.length : (int)toCopy;
- int nRead = instream.read(buffer, 0, toRead);
- if (nRead >= 0) mBytes += nRead;
- if (nRead <= 0) break;
- toCopy -= nRead;
-
- // send it to the output pipe as long as things
- // are still good
- if (pipeOkay) {
- try {
- pipe.write(buffer, 0, nRead);
- } catch (IOException e) {
- Slog.e(TAG, "Failed to write to restore pipe", e);
- pipeOkay = false;
- }
- }
- }
-
- // done sending that file! Now we just need to consume
- // the delta from info.size to the end of block.
- skipTarPadding(info.size, instream);
-
- // and now that we've sent it all, wait for the remote
- // side to acknowledge receipt
- agentSuccess = waitUntilOperationComplete(token);
- }
-
- // okay, if the remote end failed at any point, deal with
- // it by ignoring the rest of the restore on it
- if (!agentSuccess) {
- if (DEBUG) {
- Slog.d(TAG, "Agent failure restoring " + pkg + "; now ignoring");
- }
- mBackupHandler.removeMessages(MSG_RESTORE_OPERATION_TIMEOUT);
- tearDownPipes();
- tearDownAgent(mTargetApp, false);
- mPackagePolicies.put(pkg, RestorePolicy.IGNORE);
- }
- }
-
- // Problems setting up the agent communication, or an already-
- // ignored package: skip to the next tar stream entry by
- // reading and discarding this file.
- if (!okay) {
- if (DEBUG) Slog.d(TAG, "[discarding file content]");
- long bytesToConsume = (info.size + 511) & ~511;
- while (bytesToConsume > 0) {
- int toRead = (bytesToConsume > buffer.length)
- ? buffer.length : (int)bytesToConsume;
- long nRead = instream.read(buffer, 0, toRead);
- if (nRead >= 0) mBytes += nRead;
- if (nRead <= 0) break;
- bytesToConsume -= nRead;
- }
- }
- }
- }
- } catch (IOException e) {
- if (DEBUG) Slog.w(TAG, "io exception on restore socket read", e);
- // treat as EOF
- info = null;
- }
-
- return (info != null);
- }
-
- void setUpPipes() throws IOException {
- mPipes = ParcelFileDescriptor.createPipe();
- }
-
- void tearDownPipes() {
- if (mPipes != null) {
- try {
- mPipes[0].close();
- mPipes[0] = null;
- mPipes[1].close();
- mPipes[1] = null;
- } catch (IOException e) {
- Slog.w(TAG, "Couldn't close agent pipes", e);
- }
- mPipes = null;
- }
- }
-
- void tearDownAgent(ApplicationInfo app, boolean doRestoreFinished) {
- if (mAgent != null) {
- try {
- // In the adb restore case, we do restore-finished here
- if (doRestoreFinished) {
- final int token = generateRandomIntegerToken();
- final AdbRestoreFinishedLatch latch = new AdbRestoreFinishedLatch(token);
- prepareOperationTimeout(token, TIMEOUT_FULL_BACKUP_INTERVAL, latch,
- OP_TYPE_RESTORE_WAIT);
- if (mTargetApp.processName.equals("system")) {
- if (MORE_DEBUG) {
- Slog.d(TAG, "system agent - restoreFinished on thread");
- }
- Runnable runner = new RestoreFinishedRunnable(mAgent, token);
- new Thread(runner, "restore-sys-finished-runner").start();
- } else {
- mAgent.doRestoreFinished(token, mBackupManagerBinder);
- }
-
- latch.await();
- }
-
- // unbind and tidy up even on timeout or failure, just in case
- mActivityManager.unbindBackupAgent(app);
-
- // The agent was running with a stub Application object, so shut it down.
- // !!! We hardcode the confirmation UI's package name here rather than use a
- // manifest flag! TODO something less direct.
- if (app.uid >= Process.FIRST_APPLICATION_UID
- && !app.packageName.equals("com.android.backupconfirm")) {
- if (DEBUG) Slog.d(TAG, "Killing host process");
- mActivityManager.killApplicationProcess(app.processName, app.uid);
- } else {
- if (DEBUG) Slog.d(TAG, "Not killing after full restore");
- }
- } catch (RemoteException e) {
- Slog.d(TAG, "Lost app trying to shut down");
- }
- mAgent = null;
- }
- }
-
- class RestoreInstallObserver extends PackageInstallObserver {
- final AtomicBoolean mDone = new AtomicBoolean();
- String mPackageName;
- int mResult;
-
- public void reset() {
- synchronized (mDone) {
- mDone.set(false);
- }
- }
-
- public void waitForCompletion() {
- synchronized (mDone) {
- while (mDone.get() == false) {
- try {
- mDone.wait();
- } catch (InterruptedException e) { }
- }
- }
- }
-
- int getResult() {
- return mResult;
- }
-
- @Override
- public void onPackageInstalled(String packageName, int returnCode,
- String msg, Bundle extras) {
- synchronized (mDone) {
- mResult = returnCode;
- mPackageName = packageName;
- mDone.set(true);
- mDone.notifyAll();
- }
- }
- }
-
- class RestoreDeleteObserver extends IPackageDeleteObserver.Stub {
- final AtomicBoolean mDone = new AtomicBoolean();
- int mResult;
-
- public void reset() {
- synchronized (mDone) {
- mDone.set(false);
- }
- }
-
- public void waitForCompletion() {
- synchronized (mDone) {
- while (mDone.get() == false) {
- try {
- mDone.wait();
- } catch (InterruptedException e) { }
- }
- }
- }
-
- @Override
- public void packageDeleted(String packageName, int returnCode) throws RemoteException {
- synchronized (mDone) {
- mResult = returnCode;
- mDone.set(true);
- mDone.notifyAll();
- }
- }
- }
-
- final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver();
- final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver();
-
- boolean installApk(FileMetadata info, String installerPackage, InputStream instream) {
- boolean okay = true;
-
- if (DEBUG) Slog.d(TAG, "Installing from backup: " + info.packageName);
-
- // The file content is an .apk file. Copy it out to a staging location and
- // attempt to install it.
- File apkFile = new File(mDataDir, info.packageName);
- try {
- FileOutputStream apkStream = new FileOutputStream(apkFile);
- byte[] buffer = new byte[32 * 1024];
- long size = info.size;
- while (size > 0) {
- long toRead = (buffer.length < size) ? buffer.length : size;
- int didRead = instream.read(buffer, 0, (int)toRead);
- if (didRead >= 0) mBytes += didRead;
- apkStream.write(buffer, 0, didRead);
- size -= didRead;
- }
- apkStream.close();
-
- // make sure the installer can read it
- apkFile.setReadable(true, false);
-
- // Now install it
- Uri packageUri = Uri.fromFile(apkFile);
- mInstallObserver.reset();
- mPackageManager.installPackage(packageUri, mInstallObserver,
- PackageManager.INSTALL_REPLACE_EXISTING | PackageManager.INSTALL_FROM_ADB,
- installerPackage);
- mInstallObserver.waitForCompletion();
-
- if (mInstallObserver.getResult() != PackageManager.INSTALL_SUCCEEDED) {
- // The only time we continue to accept install of data even if the
- // apk install failed is if we had already determined that we could
- // accept the data regardless.
- if (mPackagePolicies.get(info.packageName) != RestorePolicy.ACCEPT) {
- okay = false;
- }
- } else {
- // Okay, the install succeeded. Make sure it was the right app.
- boolean uninstall = false;
- if (!mInstallObserver.mPackageName.equals(info.packageName)) {
- Slog.w(TAG, "Restore stream claimed to include apk for "
- + info.packageName + " but apk was really "
- + mInstallObserver.mPackageName);
- // delete the package we just put in place; it might be fraudulent
- okay = false;
- uninstall = true;
- } else {
- try {
- PackageInfo pkg = mPackageManager.getPackageInfo(info.packageName,
- PackageManager.GET_SIGNATURES);
- if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_ALLOW_BACKUP) == 0) {
- Slog.w(TAG, "Restore stream contains apk of package "
- + info.packageName + " but it disallows backup/restore");
- okay = false;
- } else {
- // So far so good -- do the signatures match the manifest?
- Signature[] sigs = mManifestSignatures.get(info.packageName);
- if (signaturesMatch(sigs, pkg)) {
- // If this is a system-uid app without a declared backup agent,
- // don't restore any of the file data.
- if ((pkg.applicationInfo.uid < Process.FIRST_APPLICATION_UID)
- && (pkg.applicationInfo.backupAgentName == null)) {
- Slog.w(TAG, "Installed app " + info.packageName
- + " has restricted uid and no agent");
- okay = false;
- }
- } else {
- Slog.w(TAG, "Installed app " + info.packageName
- + " signatures do not match restore manifest");
- okay = false;
- uninstall = true;
- }
- }
- } catch (NameNotFoundException e) {
- Slog.w(TAG, "Install of package " + info.packageName
- + " succeeded but now not found");
- okay = false;
- }
- }
-
- // If we're not okay at this point, we need to delete the package
- // that we just installed.
- if (uninstall) {
- mDeleteObserver.reset();
- mPackageManager.deletePackage(mInstallObserver.mPackageName,
- mDeleteObserver, 0);
- mDeleteObserver.waitForCompletion();
- }
- }
- } catch (IOException e) {
- Slog.e(TAG, "Unable to transcribe restored apk for install");
- okay = false;
- } finally {
- apkFile.delete();
- }
-
- return okay;
- }
-
- // Given an actual file content size, consume the post-content padding mandated
- // by the tar format.
- void skipTarPadding(long size, InputStream instream) throws IOException {
- long partial = (size + 512) % 512;
- if (partial > 0) {
- final int needed = 512 - (int)partial;
- byte[] buffer = new byte[needed];
- if (readExactly(instream, buffer, 0, needed) == needed) {
- mBytes += needed;
- } else throw new IOException("Unexpected EOF in padding");
- }
- }
-
- // Read a widget metadata file, returning the restored blob
- void readMetadata(FileMetadata info, InputStream instream) throws IOException {
- // Fail on suspiciously large widget dump files
- if (info.size > 64 * 1024) {
- throw new IOException("Metadata too big; corrupt? size=" + info.size);
- }
-
- byte[] buffer = new byte[(int) info.size];
- if (readExactly(instream, buffer, 0, (int)info.size) == info.size) {
- mBytes += info.size;
- } else throw new IOException("Unexpected EOF in widget data");
-
- String[] str = new String[1];
- int offset = extractLine(buffer, 0, str);
- int version = Integer.parseInt(str[0]);
- if (version == BACKUP_MANIFEST_VERSION) {
- offset = extractLine(buffer, offset, str);
- final String pkg = str[0];
- if (info.packageName.equals(pkg)) {
- // Data checks out -- the rest of the buffer is a concatenation of
- // binary blobs as described in the comment at writeAppWidgetData()
- ByteArrayInputStream bin = new ByteArrayInputStream(buffer,
- offset, buffer.length - offset);
- DataInputStream in = new DataInputStream(bin);
- while (bin.available() > 0) {
- int token = in.readInt();
- int size = in.readInt();
- if (size > 64 * 1024) {
- throw new IOException("Datum "
- + Integer.toHexString(token)
- + " too big; corrupt? size=" + info.size);
- }
- switch (token) {
- case BACKUP_WIDGET_METADATA_TOKEN:
- {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Got widget metadata for " + info.packageName);
- }
- mWidgetData = new byte[size];
- in.read(mWidgetData);
- break;
- }
- default:
- {
- if (DEBUG) {
- Slog.i(TAG, "Ignoring metadata blob "
- + Integer.toHexString(token)
- + " for " + info.packageName);
- }
- in.skipBytes(size);
- break;
- }
- }
- }
- } else {
- Slog.w(TAG, "Metadata mismatch: package " + info.packageName
- + " but widget data for " + pkg);
- }
- } else {
- Slog.w(TAG, "Unsupported metadata version " + version);
- }
- }
-
- // Returns a policy constant; takes a buffer arg to reduce memory churn
- RestorePolicy readAppManifest(FileMetadata info, InputStream instream)
- throws IOException {
- // Fail on suspiciously large manifest files
- if (info.size > 64 * 1024) {
- throw new IOException("Restore manifest too big; corrupt? size=" + info.size);
- }
-
- byte[] buffer = new byte[(int) info.size];
- if (readExactly(instream, buffer, 0, (int)info.size) == info.size) {
- mBytes += info.size;
- } else throw new IOException("Unexpected EOF in manifest");
-
- RestorePolicy policy = RestorePolicy.IGNORE;
- String[] str = new String[1];
- int offset = 0;
-
- try {
- offset = extractLine(buffer, offset, str);
- int version = Integer.parseInt(str[0]);
- if (version == BACKUP_MANIFEST_VERSION) {
- offset = extractLine(buffer, offset, str);
- String manifestPackage = str[0];
- // TODO: handle <original-package>
- if (manifestPackage.equals(info.packageName)) {
- offset = extractLine(buffer, offset, str);
- version = Integer.parseInt(str[0]); // app version
- offset = extractLine(buffer, offset, str);
- // This is the platform version, which we don't use, but we parse it
- // as a safety against corruption in the manifest.
- Integer.parseInt(str[0]);
- offset = extractLine(buffer, offset, str);
- info.installerPackageName = (str[0].length() > 0) ? str[0] : null;
- offset = extractLine(buffer, offset, str);
- boolean hasApk = str[0].equals("1");
- offset = extractLine(buffer, offset, str);
- int numSigs = Integer.parseInt(str[0]);
- if (numSigs > 0) {
- Signature[] sigs = new Signature[numSigs];
- for (int i = 0; i < numSigs; i++) {
- offset = extractLine(buffer, offset, str);
- sigs[i] = new Signature(str[0]);
- }
- mManifestSignatures.put(info.packageName, sigs);
-
- // Okay, got the manifest info we need...
- try {
- PackageInfo pkgInfo = mPackageManager.getPackageInfo(
- info.packageName, PackageManager.GET_SIGNATURES);
- // Fall through to IGNORE if the app explicitly disallows backup
- final int flags = pkgInfo.applicationInfo.flags;
- if ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) {
- // Restore system-uid-space packages only if they have
- // defined a custom backup agent
- if ((pkgInfo.applicationInfo.uid >= Process.FIRST_APPLICATION_UID)
- || (pkgInfo.applicationInfo.backupAgentName != null)) {
- // Verify signatures against any installed version; if they
- // don't match, then we fall though and ignore the data. The
- // signatureMatch() method explicitly ignores the signature
- // check for packages installed on the system partition, because
- // such packages are signed with the platform cert instead of
- // the app developer's cert, so they're different on every
- // device.
- if (signaturesMatch(sigs, pkgInfo)) {
- if ((pkgInfo.applicationInfo.flags
- & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) != 0) {
- Slog.i(TAG, "Package has restoreAnyVersion; taking data");
- policy = RestorePolicy.ACCEPT;
- } else if (pkgInfo.versionCode >= version) {
- Slog.i(TAG, "Sig + version match; taking data");
- policy = RestorePolicy.ACCEPT;
- } else {
- // The data is from a newer version of the app than
- // is presently installed. That means we can only
- // use it if the matching apk is also supplied.
- Slog.d(TAG, "Data version " + version
- + " is newer than installed version "
- + pkgInfo.versionCode + " - requiring apk");
- policy = RestorePolicy.ACCEPT_IF_APK;
- }
- } else {
- Slog.w(TAG, "Restore manifest signatures do not match "
- + "installed application for " + info.packageName);
- }
- } else {
- Slog.w(TAG, "Package " + info.packageName
- + " is system level with no agent");
- }
- } else {
- if (DEBUG) Slog.i(TAG, "Restore manifest from "
- + info.packageName + " but allowBackup=false");
- }
- } catch (NameNotFoundException e) {
- // Okay, the target app isn't installed. We can process
- // the restore properly only if the dataset provides the
- // apk file and we can successfully install it.
- if (DEBUG) Slog.i(TAG, "Package " + info.packageName
- + " not installed; requiring apk in dataset");
- policy = RestorePolicy.ACCEPT_IF_APK;
- }
-
- if (policy == RestorePolicy.ACCEPT_IF_APK && !hasApk) {
- Slog.i(TAG, "Cannot restore package " + info.packageName
- + " without the matching .apk");
- }
- } else {
- Slog.i(TAG, "Missing signature on backed-up package "
- + info.packageName);
- }
- } else {
- Slog.i(TAG, "Expected package " + info.packageName
- + " but restore manifest claims " + manifestPackage);
- }
- } else {
- Slog.i(TAG, "Unknown restore manifest version " + version
- + " for package " + info.packageName);
- }
- } catch (NumberFormatException e) {
- Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName);
- } catch (IllegalArgumentException e) {
- Slog.w(TAG, e.getMessage());
- }
-
- return policy;
- }
-
- // Builds a line from a byte buffer starting at 'offset', and returns
- // the index of the next unconsumed data in the buffer.
- int extractLine(byte[] buffer, int offset, String[] outStr) throws IOException {
- final int end = buffer.length;
- if (offset >= end) throw new IOException("Incomplete data");
-
- int pos;
- for (pos = offset; pos < end; pos++) {
- byte c = buffer[pos];
- // at LF we declare end of line, and return the next char as the
- // starting point for the next time through
- if (c == '\n') {
- break;
- }
- }
- outStr[0] = new String(buffer, offset, pos - offset);
- pos++; // may be pointing an extra byte past the end but that's okay
- return pos;
- }
-
- void dumpFileMetadata(FileMetadata info) {
- if (DEBUG) {
- StringBuilder b = new StringBuilder(128);
-
- // mode string
- b.append((info.type == BackupAgent.TYPE_DIRECTORY) ? 'd' : '-');
- b.append(((info.mode & 0400) != 0) ? 'r' : '-');
- b.append(((info.mode & 0200) != 0) ? 'w' : '-');
- b.append(((info.mode & 0100) != 0) ? 'x' : '-');
- b.append(((info.mode & 0040) != 0) ? 'r' : '-');
- b.append(((info.mode & 0020) != 0) ? 'w' : '-');
- b.append(((info.mode & 0010) != 0) ? 'x' : '-');
- b.append(((info.mode & 0004) != 0) ? 'r' : '-');
- b.append(((info.mode & 0002) != 0) ? 'w' : '-');
- b.append(((info.mode & 0001) != 0) ? 'x' : '-');
- b.append(String.format(" %9d ", info.size));
-
- Date stamp = new Date(info.mtime);
- b.append(new SimpleDateFormat("MMM dd HH:mm:ss ").format(stamp));
-
- b.append(info.packageName);
- b.append(" :: ");
- b.append(info.domain);
- b.append(" :: ");
- b.append(info.path);
-
- Slog.i(TAG, b.toString());
- }
- }
-
- // Consume a tar file header block [sequence] and accumulate the relevant metadata
- FileMetadata readTarHeaders(InputStream instream) throws IOException {
- byte[] block = new byte[512];
- FileMetadata info = null;
-
- boolean gotHeader = readTarHeader(instream, block);
- if (gotHeader) {
- try {
- // okay, presume we're okay, and extract the various metadata
- info = new FileMetadata();
- info.size = extractRadix(block, 124, 12, 8);
- info.mtime = extractRadix(block, 136, 12, 8);
- info.mode = extractRadix(block, 100, 8, 8);
-
- info.path = extractString(block, 345, 155); // prefix
- String path = extractString(block, 0, 100);
- if (path.length() > 0) {
- if (info.path.length() > 0) info.path += '/';
- info.path += path;
- }
-
- // tar link indicator field: 1 byte at offset 156 in the header.
- int typeChar = block[156];
- if (typeChar == 'x') {
- // pax extended header, so we need to read that
- gotHeader = readPaxExtendedHeader(instream, info);
- if (gotHeader) {
- // and after a pax extended header comes another real header -- read
- // that to find the real file type
- gotHeader = readTarHeader(instream, block);
- }
- if (!gotHeader) throw new IOException("Bad or missing pax header");
-
- typeChar = block[156];
- }
-
- switch (typeChar) {
- case '0': info.type = BackupAgent.TYPE_FILE; break;
- case '5': {
- info.type = BackupAgent.TYPE_DIRECTORY;
- if (info.size != 0) {
- Slog.w(TAG, "Directory entry with nonzero size in header");
- info.size = 0;
- }
- break;
- }
- case 0: {
- // presume EOF
- if (DEBUG) Slog.w(TAG, "Saw type=0 in tar header block, info=" + info);
- return null;
- }
- default: {
- Slog.e(TAG, "Unknown tar entity type: " + typeChar);
- throw new IOException("Unknown entity type " + typeChar);
- }
- }
-
- // Parse out the path
- //
- // first: apps/shared/unrecognized
- if (FullBackup.SHARED_PREFIX.regionMatches(0,
- info.path, 0, FullBackup.SHARED_PREFIX.length())) {
- // File in shared storage. !!! TODO: implement this.
- info.path = info.path.substring(FullBackup.SHARED_PREFIX.length());
- info.packageName = SHARED_BACKUP_AGENT_PACKAGE;
- info.domain = FullBackup.SHARED_STORAGE_TOKEN;
- if (DEBUG) Slog.i(TAG, "File in shared storage: " + info.path);
- } else if (FullBackup.APPS_PREFIX.regionMatches(0,
- info.path, 0, FullBackup.APPS_PREFIX.length())) {
- // App content! Parse out the package name and domain
-
- // strip the apps/ prefix
- info.path = info.path.substring(FullBackup.APPS_PREFIX.length());
-
- // extract the package name
- int slash = info.path.indexOf('/');
- if (slash < 0) throw new IOException("Illegal semantic path in " + info.path);
- info.packageName = info.path.substring(0, slash);
- info.path = info.path.substring(slash+1);
-
- // if it's a manifest or metadata payload we're done, otherwise parse
- // out the domain into which the file will be restored
- if (!info.path.equals(BACKUP_MANIFEST_FILENAME)
- && !info.path.equals(BACKUP_METADATA_FILENAME)) {
- slash = info.path.indexOf('/');
- if (slash < 0) throw new IOException("Illegal semantic path in non-manifest " + info.path);
- info.domain = info.path.substring(0, slash);
- info.path = info.path.substring(slash + 1);
- }
- }
- } catch (IOException e) {
- if (DEBUG) {
- Slog.e(TAG, "Parse error in header: " + e.getMessage());
- HEXLOG(block);
- }
- throw e;
- }
- }
- return info;
- }
-
- private void HEXLOG(byte[] block) {
- int offset = 0;
- int todo = block.length;
- StringBuilder buf = new StringBuilder(64);
- while (todo > 0) {
- buf.append(String.format("%04x ", offset));
- int numThisLine = (todo > 16) ? 16 : todo;
- for (int i = 0; i < numThisLine; i++) {
- buf.append(String.format("%02x ", block[offset+i]));
- }
- Slog.i("hexdump", buf.toString());
- buf.setLength(0);
- todo -= numThisLine;
- offset += numThisLine;
- }
- }
-
- // Read exactly the given number of bytes into a buffer at the stated offset.
- // Returns false if EOF is encountered before the requested number of bytes
- // could be read.
- int readExactly(InputStream in, byte[] buffer, int offset, int size)
- throws IOException {
- if (size <= 0) throw new IllegalArgumentException("size must be > 0");
-
- int soFar = 0;
- while (soFar < size) {
- int nRead = in.read(buffer, offset + soFar, size - soFar);
- if (nRead <= 0) {
- if (MORE_DEBUG) Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar);
- break;
- }
- soFar += nRead;
- }
- return soFar;
- }
-
- boolean readTarHeader(InputStream instream, byte[] block) throws IOException {
- final int got = readExactly(instream, block, 0, 512);
- if (got == 0) return false; // Clean EOF
- if (got < 512) throw new IOException("Unable to read full block header");
- mBytes += 512;
- return true;
- }
-
- // overwrites 'info' fields based on the pax extended header
- boolean readPaxExtendedHeader(InputStream instream, FileMetadata info)
- throws IOException {
- // We should never see a pax extended header larger than this
- if (info.size > 32*1024) {
- Slog.w(TAG, "Suspiciously large pax header size " + info.size
- + " - aborting");
- throw new IOException("Sanity failure: pax header size " + info.size);
- }
-
- // read whole blocks, not just the content size
- int numBlocks = (int)((info.size + 511) >> 9);
- byte[] data = new byte[numBlocks * 512];
- if (readExactly(instream, data, 0, data.length) < data.length) {
- throw new IOException("Unable to read full pax header");
- }
- mBytes += data.length;
-
- final int contentSize = (int) info.size;
- int offset = 0;
- do {
- // extract the line at 'offset'
- int eol = offset+1;
- while (eol < contentSize && data[eol] != ' ') eol++;
- if (eol >= contentSize) {
- // error: we just hit EOD looking for the end of the size field
- throw new IOException("Invalid pax data");
- }
- // eol points to the space between the count and the key
- int linelen = (int) extractRadix(data, offset, eol - offset, 10);
- int key = eol + 1; // start of key=value
- eol = offset + linelen - 1; // trailing LF
- int value;
- for (value = key+1; data[value] != '=' && value <= eol; value++);
- if (value > eol) {
- throw new IOException("Invalid pax declaration");
- }
-
- // pax requires that key/value strings be in UTF-8
- String keyStr = new String(data, key, value-key, "UTF-8");
- // -1 to strip the trailing LF
- String valStr = new String(data, value+1, eol-value-1, "UTF-8");
-
- if ("path".equals(keyStr)) {
- info.path = valStr;
- } else if ("size".equals(keyStr)) {
- info.size = Long.parseLong(valStr);
- } else {
- if (DEBUG) Slog.i(TAG, "Unhandled pax key: " + key);
- }
-
- offset += linelen;
- } while (offset < contentSize);
-
- return true;
- }
-
- long extractRadix(byte[] data, int offset, int maxChars, int radix)
- throws IOException {
- long value = 0;
- final int end = offset + maxChars;
- for (int i = offset; i < end; i++) {
- final byte b = data[i];
- // Numeric fields in tar can terminate with either NUL or SPC
- if (b == 0 || b == ' ') break;
- if (b < '0' || b > ('0' + radix - 1)) {
- throw new IOException("Invalid number in header: '" + (char)b + "' for radix " + radix);
- }
- value = radix * value + (b - '0');
- }
- return value;
- }
-
- String extractString(byte[] data, int offset, int maxChars) throws IOException {
- final int end = offset + maxChars;
- int eos = offset;
- // tar string fields terminate early with a NUL
- while (eos < end && data[eos] != 0) eos++;
- return new String(data, offset, eos-offset, "US-ASCII");
- }
-
- void sendStartRestore() {
- if (mObserver != null) {
- try {
- mObserver.onStartRestore();
- } catch (RemoteException e) {
- Slog.w(TAG, "full restore observer went away: startRestore");
- mObserver = null;
- }
- }
- }
-
- void sendOnRestorePackage(String name) {
- if (mObserver != null) {
- try {
- // TODO: use a more user-friendly name string
- mObserver.onRestorePackage(name);
- } catch (RemoteException e) {
- Slog.w(TAG, "full restore observer went away: restorePackage");
- mObserver = null;
- }
- }
- }
-
- void sendEndRestore() {
- if (mObserver != null) {
- try {
- mObserver.onEndRestore();
- } catch (RemoteException e) {
- Slog.w(TAG, "full restore observer went away: endRestore");
- mObserver = null;
- }
- }
- }
- }
-
- // ----- Restore handling -----
-
- /**
- * Returns whether the signatures stored {@param storedSigs}, coming from the source apk, match
- * the signatures of the apk installed on the device, the target apk. If the target resides in
- * the system partition we return true. Otherwise it's considered a match if both conditions
- * hold:
- *
- * <ul>
- * <li>Source and target have at least one signature each
- * <li>Target contains all signatures in source
- * </ul>
- *
- * Note that if {@param target} is null we return false.
- */
- static boolean signaturesMatch(Signature[] storedSigs, PackageInfo target) {
- if (target == null) {
- return false;
- }
-
- // If the target resides on the system partition, we allow it to restore
- // data from the like-named package in a restore set even if the signatures
- // do not match. (Unlike general applications, those flashed to the system
- // partition will be signed with the device's platform certificate, so on
- // different phones the same system app will have different signatures.)
- if ((target.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
- if (MORE_DEBUG) {
- Slog.v(TAG, "System app " + target.packageName + " - skipping sig check");
- }
- return true;
- }
-
- Signature[] deviceSigs = target.signatures;
- if (MORE_DEBUG) {
- Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs + " device=" + deviceSigs);
- }
-
- // Don't allow unsigned apps on either end
- if (ArrayUtils.isEmpty(storedSigs) || ArrayUtils.isEmpty(deviceSigs)) {
- return false;
- }
-
- // Signatures can be added over time, so the target-device apk needs to contain all the
- // source-device apk signatures, but not necessarily the other way around.
- int nStored = storedSigs.length;
- int nDevice = deviceSigs.length;
-
- for (int i=0; i < nStored; i++) {
- boolean match = false;
- for (int j=0; j < nDevice; j++) {
- if (storedSigs[i].equals(deviceSigs[j])) {
- match = true;
- break;
- }
- }
- if (!match) {
- return false;
- }
- }
- return true;
- }
-
- // Used by both incremental and full restore
- void restoreWidgetData(String packageName, byte[] widgetData) {
- // Apply the restored widget state and generate the ID update for the app
- // TODO: http://b/22388012
- if (MORE_DEBUG) {
- Slog.i(TAG, "Incorporating restored widget data");
- }
- AppWidgetBackupBridge.restoreWidgetState(packageName, widgetData, UserHandle.USER_SYSTEM);
- }
-
- // *****************************
- // NEW UNIFIED RESTORE IMPLEMENTATION
- // *****************************
-
- // states of the unified-restore state machine
- enum UnifiedRestoreState {
- INITIAL,
- RUNNING_QUEUE,
- RESTORE_KEYVALUE,
- RESTORE_FULL,
- RESTORE_FINISHED,
- FINAL
- }
-
- class PerformUnifiedRestoreTask implements BackupRestoreTask {
- // Transport we're working with to do the restore
- private IBackupTransport mTransport;
-
- // Where per-transport saved state goes
- File mStateDir;
-
- // Restore observer; may be null
- private IRestoreObserver mObserver;
-
- // BackuoManagerMonitor; may be null
- private IBackupManagerMonitor mMonitor;
-
- // Token identifying the dataset to the transport
- private long mToken;
-
- // When this is a restore-during-install, this is the token identifying the
- // operation to the Package Manager, and we must ensure that we let it know
- // when we're finished.
- private int mPmToken;
-
- // When this is restore-during-install, we need to tell the package manager
- // whether we actually launched the app, because this affects notifications
- // around externally-visible state transitions.
- private boolean mDidLaunch;
-
- // Is this a whole-system restore, i.e. are we establishing a new ancestral
- // dataset to base future restore-at-install operations from?
- private boolean mIsSystemRestore;
-
- // If this is a single-package restore, what package are we interested in?
- private PackageInfo mTargetPackage;
-
- // In all cases, the calculated list of packages that we are trying to restore
- private List<PackageInfo> mAcceptSet;
-
- // Our bookkeeping about the ancestral dataset
- private PackageManagerBackupAgent mPmAgent;
-
- // Currently-bound backup agent for restore + restoreFinished purposes
- private IBackupAgent mAgent;
-
- // What sort of restore we're doing now
- private RestoreDescription mRestoreDescription;
-
- // The package we're currently restoring
- private PackageInfo mCurrentPackage;
-
- // Widget-related data handled as part of this restore operation
- private byte[] mWidgetData;
-
- // Number of apps restored in this pass
- private int mCount;
-
- // When did we start?
- private long mStartRealtime;
-
- // State machine progress
- private UnifiedRestoreState mState;
-
- // How are things going?
- private int mStatus;
-
- // Done?
- private boolean mFinished;
-
- // Key/value: bookkeeping about staged data and files for agent access
- private File mBackupDataName;
- private File mStageName;
- private File mSavedStateName;
- private File mNewStateName;
- ParcelFileDescriptor mBackupData;
- ParcelFileDescriptor mNewState;
-
- private final int mEphemeralOpToken;
-
- // Invariant: mWakelock is already held, and this task is responsible for
- // releasing it at the end of the restore operation.
- PerformUnifiedRestoreTask(IBackupTransport transport, IRestoreObserver observer,
- IBackupManagerMonitor monitor, long restoreSetToken, PackageInfo targetPackage,
- int pmToken, boolean isFullSystemRestore, String[] filterSet) {
- mEphemeralOpToken = generateRandomIntegerToken();
- mState = UnifiedRestoreState.INITIAL;
- mStartRealtime = SystemClock.elapsedRealtime();
-
- mTransport = transport;
- mObserver = observer;
- mMonitor = monitor;
- mToken = restoreSetToken;
- mPmToken = pmToken;
- mTargetPackage = targetPackage;
- mIsSystemRestore = isFullSystemRestore;
- mFinished = false;
- mDidLaunch = false;
-
- if (targetPackage != null) {
- // Single package restore
- mAcceptSet = new ArrayList<PackageInfo>();
- mAcceptSet.add(targetPackage);
- } else {
- // Everything possible, or a target set
- if (filterSet == null) {
- // We want everything and a pony
- List<PackageInfo> apps =
- PackageManagerBackupAgent.getStorableApplications(mPackageManager);
- filterSet = packagesToNames(apps);
- if (DEBUG) {
- Slog.i(TAG, "Full restore; asking about " + filterSet.length + " apps");
- }
- }
-
- mAcceptSet = new ArrayList<PackageInfo>(filterSet.length);
-
- // Pro tem, we insist on moving the settings provider package to last place.
- // Keep track of whether it's in the list, and bump it down if so. We also
- // want to do the system package itself first if it's called for.
- boolean hasSystem = false;
- boolean hasSettings = false;
- for (int i = 0; i < filterSet.length; i++) {
- try {
- PackageInfo info = mPackageManager.getPackageInfo(filterSet[i], 0);
- if ("android".equals(info.packageName)) {
- hasSystem = true;
- continue;
- }
- if (SETTINGS_PACKAGE.equals(info.packageName)) {
- hasSettings = true;
- continue;
- }
-
- if (appIsEligibleForBackup(info.applicationInfo, mPackageManager)) {
- mAcceptSet.add(info);
- }
- } catch (NameNotFoundException e) {
- // requested package name doesn't exist; ignore it
- }
- }
- if (hasSystem) {
- try {
- mAcceptSet.add(0, mPackageManager.getPackageInfo("android", 0));
- } catch (NameNotFoundException e) {
- // won't happen; we know a priori that it's valid
- }
- }
- if (hasSettings) {
- try {
- mAcceptSet.add(mPackageManager.getPackageInfo(SETTINGS_PACKAGE, 0));
- } catch (NameNotFoundException e) {
- // this one is always valid too
- }
- }
- }
-
- if (MORE_DEBUG) {
- Slog.v(TAG, "Restore; accept set size is " + mAcceptSet.size());
- for (PackageInfo info : mAcceptSet) {
- Slog.v(TAG, " " + info.packageName);
- }
- }
- }
-
- private String[] packagesToNames(List<PackageInfo> apps) {
- final int N = apps.size();
- String[] names = new String[N];
- for (int i = 0; i < N; i++) {
- names[i] = apps.get(i).packageName;
- }
- return names;
- }
-
- // Execute one tick of whatever state machine the task implements
- @Override
- public void execute() {
- if (MORE_DEBUG) Slog.v(TAG, "*** Executing restore step " + mState);
- switch (mState) {
- case INITIAL:
- startRestore();
- break;
-
- case RUNNING_QUEUE:
- dispatchNextRestore();
- break;
-
- case RESTORE_KEYVALUE:
- restoreKeyValue();
- break;
-
- case RESTORE_FULL:
- restoreFull();
- break;
-
- case RESTORE_FINISHED:
- restoreFinished();
- break;
-
- case FINAL:
- if (!mFinished) finalizeRestore();
- else {
- Slog.e(TAG, "Duplicate finish");
- }
- mFinished = true;
- break;
- }
- }
-
- /*
- * SKETCH OF OPERATION
- *
- * create one of these PerformUnifiedRestoreTask objects, telling it which
- * dataset & transport to address, and then parameters within the restore
- * operation: single target package vs many, etc.
- *
- * 1. transport.startRestore(token, list-of-packages). If we need @pm@ it is
- * always placed first and the settings provider always placed last [for now].
- *
- * 1a [if we needed @pm@ then nextRestorePackage() and restore the PMBA inline]
- *
- * [ state change => RUNNING_QUEUE ]
- *
- * NOW ITERATE:
- *
- * { 3. t.nextRestorePackage()
- * 4. does the metadata for this package allow us to restore it?
- * does the on-disk app permit us to restore it? [re-check allowBackup etc]
- * 5. is this a key/value dataset? => key/value agent restore
- * [ state change => RESTORE_KEYVALUE ]
- * 5a. spin up agent
- * 5b. t.getRestoreData() to stage it properly
- * 5c. call into agent to perform restore
- * 5d. tear down agent
- * [ state change => RUNNING_QUEUE ]
- *
- * 6. else it's a stream dataset:
- * [ state change => RESTORE_FULL ]
- * 6a. instantiate the engine for a stream restore: engine handles agent lifecycles
- * 6b. spin off engine runner on separate thread
- * 6c. ITERATE getNextFullRestoreDataChunk() and copy data to engine runner socket
- * [ state change => RUNNING_QUEUE ]
- * }
- *
- * [ state change => FINAL ]
- *
- * 7. t.finishRestore(), release wakelock, etc.
- *
- *
- */
-
- // state INITIAL : set up for the restore and read the metadata if necessary
- private void startRestore() {
- sendStartRestore(mAcceptSet.size());
-
- // If we're starting a full-system restore, set up to begin widget ID remapping
- if (mIsSystemRestore) {
- // TODO: http://b/22388012
- AppWidgetBackupBridge.restoreStarting(UserHandle.USER_SYSTEM);
- }
-
- try {
- String transportDir = mTransport.transportDirName();
- mStateDir = new File(mBaseStateDir, transportDir);
-
- // Fetch the current metadata from the dataset first
- PackageInfo pmPackage = new PackageInfo();
- pmPackage.packageName = PACKAGE_MANAGER_SENTINEL;
- mAcceptSet.add(0, pmPackage);
-
- PackageInfo[] packages = mAcceptSet.toArray(new PackageInfo[0]);
- mStatus = mTransport.startRestore(mToken, packages);
- if (mStatus != BackupTransport.TRANSPORT_OK) {
- Slog.e(TAG, "Transport error " + mStatus + "; no restore possible");
- mStatus = BackupTransport.TRANSPORT_ERROR;
- executeNextState(UnifiedRestoreState.FINAL);
- return;
- }
-
- RestoreDescription desc = mTransport.nextRestorePackage();
- if (desc == null) {
- Slog.e(TAG, "No restore metadata available; halting");
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_NO_RESTORE_METADATA_AVAILABLE,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
- mStatus = BackupTransport.TRANSPORT_ERROR;
- executeNextState(UnifiedRestoreState.FINAL);
- return;
- }
- if (!PACKAGE_MANAGER_SENTINEL.equals(desc.getPackageName())) {
- Slog.e(TAG, "Required package metadata but got "
- + desc.getPackageName());
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_NO_PM_METADATA_RECEIVED,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
- mStatus = BackupTransport.TRANSPORT_ERROR;
- executeNextState(UnifiedRestoreState.FINAL);
- return;
- }
-
- // Pull the Package Manager metadata from the restore set first
- mCurrentPackage = new PackageInfo();
- mCurrentPackage.packageName = PACKAGE_MANAGER_SENTINEL;
- mPmAgent = makeMetadataAgent(null);
- mAgent = IBackupAgent.Stub.asInterface(mPmAgent.onBind());
- if (MORE_DEBUG) {
- Slog.v(TAG, "initiating restore for PMBA");
- }
- initiateOneRestore(mCurrentPackage, 0);
- // The PM agent called operationComplete() already, because our invocation
- // of it is process-local and therefore synchronous. That means that the
- // next-state message (RUNNING_QUEUE) is already enqueued. Only if we're
- // unable to proceed with running the queue do we remove that pending
- // message and jump straight to the FINAL state. Because this was
- // synchronous we also know that we should cancel the pending timeout
- // message.
- mBackupHandler.removeMessages(MSG_RESTORE_OPERATION_TIMEOUT);
-
- // Verify that the backup set includes metadata. If not, we can't do
- // signature/version verification etc, so we simply do not proceed with
- // the restore operation.
- if (!mPmAgent.hasMetadata()) {
- Slog.e(TAG, "PM agent has no metadata, so not restoring");
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_PM_AGENT_HAS_NO_METADATA,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
- PACKAGE_MANAGER_SENTINEL,
- "Package manager restore metadata missing");
- mStatus = BackupTransport.TRANSPORT_ERROR;
- mBackupHandler.removeMessages(MSG_BACKUP_RESTORE_STEP, this);
- executeNextState(UnifiedRestoreState.FINAL);
- return;
- }
-
- // Success; cache the metadata and continue as expected with the
- // next state already enqueued
-
- } catch (Exception e) {
- // If we lost the transport at any time, halt
- Slog.e(TAG, "Unable to contact transport for restore: " + e.getMessage());
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_LOST_TRANSPORT,
- null,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT, null);
- mStatus = BackupTransport.TRANSPORT_ERROR;
- mBackupHandler.removeMessages(MSG_BACKUP_RESTORE_STEP, this);
- executeNextState(UnifiedRestoreState.FINAL);
- return;
- }
- }
-
- // state RUNNING_QUEUE : figure out what the next thing to be restored is,
- // and fire the appropriate next step
- private void dispatchNextRestore() {
- UnifiedRestoreState nextState = UnifiedRestoreState.FINAL;
- try {
- mRestoreDescription = mTransport.nextRestorePackage();
- final String pkgName = (mRestoreDescription != null)
- ? mRestoreDescription.getPackageName() : null;
- if (pkgName == null) {
- Slog.e(TAG, "Failure getting next package name");
- EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
- nextState = UnifiedRestoreState.FINAL;
- return;
- } else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) {
- // Yay we've reached the end cleanly
- if (DEBUG) {
- Slog.v(TAG, "No more packages; finishing restore");
- }
- int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime);
- EventLog.writeEvent(EventLogTags.RESTORE_SUCCESS, mCount, millis);
- nextState = UnifiedRestoreState.FINAL;
- return;
- }
-
- if (DEBUG) {
- Slog.i(TAG, "Next restore package: " + mRestoreDescription);
- }
- sendOnRestorePackage(pkgName);
-
- Metadata metaInfo = mPmAgent.getRestoredMetadata(pkgName);
- if (metaInfo == null) {
- Slog.e(TAG, "No metadata for " + pkgName);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName,
- "Package metadata missing");
- nextState = UnifiedRestoreState.RUNNING_QUEUE;
- return;
- }
-
- try {
- mCurrentPackage = mPackageManager.getPackageInfo(
- pkgName, PackageManager.GET_SIGNATURES);
- } catch (NameNotFoundException e) {
- // Whoops, we thought we could restore this package but it
- // turns out not to be present. Skip it.
- Slog.e(TAG, "Package not present: " + pkgName);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_PRESENT,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, pkgName,
- "Package missing on device");
- nextState = UnifiedRestoreState.RUNNING_QUEUE;
- return;
- }
-
- if (metaInfo.versionCode > mCurrentPackage.versionCode) {
- // Data is from a "newer" version of the app than we have currently
- // installed. If the app has not declared that it is prepared to
- // handle this case, we do not attempt the restore.
- if ((mCurrentPackage.applicationInfo.flags
- & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) == 0) {
- String message = "Source version " + metaInfo.versionCode
- + " > installed version " + mCurrentPackage.versionCode;
- Slog.w(TAG, "Package " + pkgName + ": " + message);
- Bundle monitoringExtras = putMonitoringExtra(null,
- BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION,
- metaInfo.versionCode);
- monitoringExtras = putMonitoringExtra(monitoringExtras,
- BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, false);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- monitoringExtras);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
- pkgName, message);
- nextState = UnifiedRestoreState.RUNNING_QUEUE;
- return;
- } else {
- if (DEBUG) Slog.v(TAG, "Source version " + metaInfo.versionCode
- + " > installed version " + mCurrentPackage.versionCode
- + " but restoreAnyVersion");
- Bundle monitoringExtras = putMonitoringExtra(null,
- BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION,
- metaInfo.versionCode);
- monitoringExtras = putMonitoringExtra(monitoringExtras,
- BackupManagerMonitor.EXTRA_LOG_RESTORE_ANYWAY, true);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_RESTORE_VERSION_HIGHER,
- mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- monitoringExtras);
- }
- }
-
- if (MORE_DEBUG) Slog.v(TAG, "Package " + pkgName
- + " restore version [" + metaInfo.versionCode
- + "] is compatible with installed version ["
- + mCurrentPackage.versionCode + "]");
-
- // Reset per-package preconditions and fire the appropriate next state
- mWidgetData = null;
- final int type = mRestoreDescription.getDataType();
- if (type == RestoreDescription.TYPE_KEY_VALUE) {
- nextState = UnifiedRestoreState.RESTORE_KEYVALUE;
- } else if (type == RestoreDescription.TYPE_FULL_STREAM) {
- nextState = UnifiedRestoreState.RESTORE_FULL;
- } else {
- // Unknown restore type; ignore this package and move on
- Slog.e(TAG, "Unrecognized restore type " + type);
- nextState = UnifiedRestoreState.RUNNING_QUEUE;
- return;
- }
- } catch (Exception e) {
- Slog.e(TAG, "Can't get next restore target from transport; halting: "
- + e.getMessage());
- EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
- nextState = UnifiedRestoreState.FINAL;
- return;
- } finally {
- executeNextState(nextState);
- }
- }
-
- // state RESTORE_KEYVALUE : restore one package via key/value API set
- private void restoreKeyValue() {
- // Initiating the restore will pass responsibility for the state machine's
- // progress to the agent callback, so we do not always execute the
- // next state here.
- final String packageName = mCurrentPackage.packageName;
- // Validate some semantic requirements that apply in this way
- // only to the key/value restore API flow
- if (mCurrentPackage.applicationInfo.backupAgentName == null
- || "".equals(mCurrentPackage.applicationInfo.backupAgentName)) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "Data exists for package " + packageName
- + " but app has no agent; skipping");
- }
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_APP_HAS_NO_AGENT, mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
- "Package has no agent");
- executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
- return;
- }
-
- Metadata metaInfo = mPmAgent.getRestoredMetadata(packageName);
- if (!BackupUtils.signaturesMatch(metaInfo.sigHashes, mCurrentPackage)) {
- Slog.w(TAG, "Signature mismatch restoring " + packageName);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_SIGNATURE_MISMATCH, mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
- "Signature mismatch");
- executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
- return;
- }
-
- // Good to go! Set up and bind the agent...
- mAgent = bindToAgentSynchronous(
- mCurrentPackage.applicationInfo,
- ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL);
- if (mAgent == null) {
- Slog.w(TAG, "Can't find backup agent for " + packageName);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_CANT_FIND_AGENT, mCurrentPackage,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, null);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE, packageName,
- "Restore agent missing");
- executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
- return;
- }
-
- // Whatever happens next, we've launched the target app now; remember that.
- mDidLaunch = true;
-
- // And then finally start the restore on this agent
- try {
- initiateOneRestore(mCurrentPackage, metaInfo.versionCode);
- ++mCount;
- } catch (Exception e) {
- Slog.e(TAG, "Error when attempting restore: " + e.toString());
- keyValueAgentErrorCleanup();
- executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
- }
- }
-
- // Guts of a key/value restore operation
- void initiateOneRestore(PackageInfo app, int appVersionCode) {
- final String packageName = app.packageName;
-
- if (DEBUG) Slog.d(TAG, "initiateOneRestore packageName=" + packageName);
-
- // !!! TODO: get the dirs from the transport
- mBackupDataName = new File(mDataDir, packageName + ".restore");
- mStageName = new File(mDataDir, packageName + ".stage");
- mNewStateName = new File(mStateDir, packageName + ".new");
- mSavedStateName = new File(mStateDir, packageName);
-
- // don't stage the 'android' package where the wallpaper data lives. this is
- // an optimization: we know there's no widget data hosted/published by that
- // package, and this way we avoid doing a spurious copy of MB-sized wallpaper
- // data following the download.
- boolean staging = !packageName.equals("android");
- ParcelFileDescriptor stage;
- File downloadFile = (staging) ? mStageName : mBackupDataName;
-
- try {
- // Run the transport's restore pass
- stage = ParcelFileDescriptor.open(downloadFile,
- ParcelFileDescriptor.MODE_READ_WRITE |
- ParcelFileDescriptor.MODE_CREATE |
- ParcelFileDescriptor.MODE_TRUNCATE);
-
- if (mTransport.getRestoreData(stage) != BackupTransport.TRANSPORT_OK) {
- // Transport-level failure, so we wind everything up and
- // terminate the restore operation.
- Slog.e(TAG, "Error getting restore data for " + packageName);
- EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
- stage.close();
- downloadFile.delete();
- executeNextState(UnifiedRestoreState.FINAL);
- return;
- }
-
- // We have the data from the transport. Now we extract and strip
- // any per-package metadata (typically widget-related information)
- // if appropriate
- if (staging) {
- stage.close();
- stage = ParcelFileDescriptor.open(downloadFile,
- ParcelFileDescriptor.MODE_READ_ONLY);
-
- mBackupData = ParcelFileDescriptor.open(mBackupDataName,
- ParcelFileDescriptor.MODE_READ_WRITE |
- ParcelFileDescriptor.MODE_CREATE |
- ParcelFileDescriptor.MODE_TRUNCATE);
-
- BackupDataInput in = new BackupDataInput(stage.getFileDescriptor());
- BackupDataOutput out = new BackupDataOutput(mBackupData.getFileDescriptor());
- byte[] buffer = new byte[8192]; // will grow when needed
- while (in.readNextHeader()) {
- final String key = in.getKey();
- final int size = in.getDataSize();
-
- // is this a special key?
- if (key.equals(KEY_WIDGET_STATE)) {
- if (DEBUG) {
- Slog.i(TAG, "Restoring widget state for " + packageName);
- }
- mWidgetData = new byte[size];
- in.readEntityData(mWidgetData, 0, size);
- } else {
- if (size > buffer.length) {
- buffer = new byte[size];
- }
- in.readEntityData(buffer, 0, size);
- out.writeEntityHeader(key, size);
- out.writeEntityData(buffer, size);
- }
- }
-
- mBackupData.close();
- }
-
- // Okay, we have the data. Now have the agent do the restore.
- stage.close();
-
- mBackupData = ParcelFileDescriptor.open(mBackupDataName,
- ParcelFileDescriptor.MODE_READ_ONLY);
-
- mNewState = ParcelFileDescriptor.open(mNewStateName,
- ParcelFileDescriptor.MODE_READ_WRITE |
- ParcelFileDescriptor.MODE_CREATE |
- ParcelFileDescriptor.MODE_TRUNCATE);
-
- // Kick off the restore, checking for hung agents. The timeout or
- // the operationComplete() callback will schedule the next step,
- // so we do not do that here.
- prepareOperationTimeout(mEphemeralOpToken, TIMEOUT_RESTORE_INTERVAL,
- this, OP_TYPE_RESTORE_WAIT);
- mAgent.doRestore(mBackupData, appVersionCode, mNewState,
- mEphemeralOpToken, mBackupManagerBinder);
- } catch (Exception e) {
- Slog.e(TAG, "Unable to call app for restore: " + packageName, e);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
- packageName, e.toString());
- keyValueAgentErrorCleanup(); // clears any pending timeout messages as well
-
- // After a restore failure we go back to running the queue. If there
- // are no more packages to be restored that will be handled by the
- // next step.
- executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
- }
- }
-
- // state RESTORE_FULL : restore one package via streaming engine
- private void restoreFull() {
- // None of this can run on the work looper here, so we spin asynchronous
- // work like this:
- //
- // StreamFeederThread: read data from mTransport.getNextFullRestoreDataChunk()
- // write it into the pipe to the engine
- // EngineThread: FullRestoreEngine thread communicating with the target app
- //
- // When finished, StreamFeederThread executes next state as appropriate on the
- // backup looper, and the overall unified restore task resumes
- try {
- StreamFeederThread feeder = new StreamFeederThread();
- if (MORE_DEBUG) {
- Slog.i(TAG, "Spinning threads for stream restore of "
- + mCurrentPackage.packageName);
- }
- new Thread(feeder, "unified-stream-feeder").start();
-
- // At this point the feeder is responsible for advancing the restore
- // state, so we're done here.
- } catch (IOException e) {
- // Unable to instantiate the feeder thread -- we need to bail on the
- // current target. We haven't asked the transport for data yet, though,
- // so we can do that simply by going back to running the restore queue.
- Slog.e(TAG, "Unable to construct pipes for stream restore!");
- executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
- }
- }
-
- // state RESTORE_FINISHED : provide the "no more data" signpost callback at the end
- private void restoreFinished() {
- if (DEBUG) {
- Slog.d(TAG, "restoreFinished packageName=" + mCurrentPackage.packageName);
- }
- try {
- prepareOperationTimeout(mEphemeralOpToken, TIMEOUT_RESTORE_FINISHED_INTERVAL, this,
- OP_TYPE_RESTORE_WAIT);
- mAgent.doRestoreFinished(mEphemeralOpToken, mBackupManagerBinder);
- // If we get this far, the callback or timeout will schedule the
- // next restore state, so we're done
- } catch (Exception e) {
- final String packageName = mCurrentPackage.packageName;
- Slog.e(TAG, "Unable to finalize restore of " + packageName);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
- packageName, e.toString());
- keyValueAgentErrorCleanup();
- executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
- }
- }
-
- class StreamFeederThread extends RestoreEngine implements Runnable, BackupRestoreTask {
- final String TAG = "StreamFeederThread";
- FullRestoreEngine mEngine;
- EngineThread mEngineThread;
-
- // pipe through which we read data from the transport. [0] read, [1] write
- ParcelFileDescriptor[] mTransportPipes;
-
- // pipe through which the engine will read data. [0] read, [1] write
- ParcelFileDescriptor[] mEnginePipes;
-
- private final int mEphemeralOpToken;
-
- public StreamFeederThread() throws IOException {
- mEphemeralOpToken = generateRandomIntegerToken();
- mTransportPipes = ParcelFileDescriptor.createPipe();
- mEnginePipes = ParcelFileDescriptor.createPipe();
- setRunning(true);
- }
-
- @Override
- public void run() {
- UnifiedRestoreState nextState = UnifiedRestoreState.RUNNING_QUEUE;
- int status = BackupTransport.TRANSPORT_OK;
-
- EventLog.writeEvent(EventLogTags.FULL_RESTORE_PACKAGE,
- mCurrentPackage.packageName);
-
- mEngine = new FullRestoreEngine(this, null, mMonitor, mCurrentPackage, false, false, mEphemeralOpToken);
- mEngineThread = new EngineThread(mEngine, mEnginePipes[0]);
-
- ParcelFileDescriptor eWriteEnd = mEnginePipes[1];
- ParcelFileDescriptor tReadEnd = mTransportPipes[0];
- ParcelFileDescriptor tWriteEnd = mTransportPipes[1];
-
- int bufferSize = 32 * 1024;
- byte[] buffer = new byte[bufferSize];
- FileOutputStream engineOut = new FileOutputStream(eWriteEnd.getFileDescriptor());
- FileInputStream transportIn = new FileInputStream(tReadEnd.getFileDescriptor());
-
- // spin up the engine and start moving data to it
- new Thread(mEngineThread, "unified-restore-engine").start();
-
- try {
- while (status == BackupTransport.TRANSPORT_OK) {
- // have the transport write some of the restoring data to us
- int result = mTransport.getNextFullRestoreDataChunk(tWriteEnd);
- if (result > 0) {
- // The transport wrote this many bytes of restore data to the
- // pipe, so pass it along to the engine.
- if (MORE_DEBUG) {
- Slog.v(TAG, " <- transport provided chunk size " + result);
- }
- if (result > bufferSize) {
- bufferSize = result;
- buffer = new byte[bufferSize];
- }
- int toCopy = result;
- while (toCopy > 0) {
- int n = transportIn.read(buffer, 0, toCopy);
- engineOut.write(buffer, 0, n);
- toCopy -= n;
- if (MORE_DEBUG) {
- Slog.v(TAG, " -> wrote " + n + " to engine, left=" + toCopy);
- }
- }
- } else if (result == BackupTransport.NO_MORE_DATA) {
- // Clean finish. Wind up and we're done!
- if (MORE_DEBUG) {
- Slog.i(TAG, "Got clean full-restore EOF for "
- + mCurrentPackage.packageName);
- }
- status = BackupTransport.TRANSPORT_OK;
- break;
- } else {
- // Transport reported some sort of failure; the fall-through
- // handling will deal properly with that.
- Slog.e(TAG, "Error " + result + " streaming restore for "
- + mCurrentPackage.packageName);
- EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
- status = result;
- }
- }
- if (MORE_DEBUG) Slog.v(TAG, "Done copying to engine, falling through");
- } catch (IOException e) {
- // We lost our ability to communicate via the pipes. That's worrying
- // but potentially recoverable; abandon this package's restore but
- // carry on with the next restore target.
- Slog.e(TAG, "Unable to route data for restore");
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
- mCurrentPackage.packageName, "I/O error on pipes");
- status = BackupTransport.AGENT_ERROR;
- } catch (Exception e) {
- // The transport threw; terminate the whole operation. Closing
- // the sockets will wake up the engine and it will then tidy up the
- // remote end.
- Slog.e(TAG, "Transport failed during restore: " + e.getMessage());
- EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
- status = BackupTransport.TRANSPORT_ERROR;
- } finally {
- // Close the transport pipes and *our* end of the engine pipe,
- // but leave the engine thread's end open so that it properly
- // hits EOF and winds up its operations.
- IoUtils.closeQuietly(mEnginePipes[1]);
- IoUtils.closeQuietly(mTransportPipes[0]);
- IoUtils.closeQuietly(mTransportPipes[1]);
-
- // Don't proceed until the engine has wound up operations
- mEngineThread.waitForResult();
-
- // Now we're really done with this one too
- IoUtils.closeQuietly(mEnginePipes[0]);
-
- // In all cases we want to remember whether we launched
- // the target app as part of our work so far.
- mDidLaunch = (mEngine.getAgent() != null);
-
- // If we hit a transport-level error, we are done with everything;
- // if we hit an agent error we just go back to running the queue.
- if (status == BackupTransport.TRANSPORT_OK) {
- // Clean finish means we issue the restore-finished callback
- nextState = UnifiedRestoreState.RESTORE_FINISHED;
-
- // the engine bound the target's agent, so recover that binding
- // to use for the callback.
- mAgent = mEngine.getAgent();
-
- // and the restored widget data, if any
- mWidgetData = mEngine.getWidgetData();
- } else {
- // Something went wrong somewhere. Whether it was at the transport
- // level is immaterial; we need to tell the transport to bail
- try {
- mTransport.abortFullRestore();
- } catch (Exception e) {
- // transport itself is dead; make sure we handle this as a
- // fatal error
- Slog.e(TAG, "Transport threw from abortFullRestore: " + e.getMessage());
- status = BackupTransport.TRANSPORT_ERROR;
- }
-
- // We also need to wipe the current target's data, as it's probably
- // in an incoherent state.
- clearApplicationDataSynchronous(mCurrentPackage.packageName);
-
- // Schedule the next state based on the nature of our failure
- if (status == BackupTransport.TRANSPORT_ERROR) {
- nextState = UnifiedRestoreState.FINAL;
- } else {
- nextState = UnifiedRestoreState.RUNNING_QUEUE;
- }
- }
- executeNextState(nextState);
- setRunning(false);
- }
- }
-
- // BackupRestoreTask interface, specifically for timeout handling
-
- @Override
- public void execute() { /* intentionally empty */ }
-
- @Override
- public void operationComplete(long result) { /* intentionally empty */ }
-
- // The app has timed out handling a restoring file
- @Override
- public void handleCancel(boolean cancelAll) {
- removeOperation(mEphemeralOpToken);
- if (DEBUG) {
- Slog.w(TAG, "Full-data restore target timed out; shutting down");
- }
-
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_TIMEOUT,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
- mEngineThread.handleTimeout();
-
- IoUtils.closeQuietly(mEnginePipes[1]);
- mEnginePipes[1] = null;
- IoUtils.closeQuietly(mEnginePipes[0]);
- mEnginePipes[0] = null;
- }
- }
-
- class EngineThread implements Runnable {
- FullRestoreEngine mEngine;
- FileInputStream mEngineStream;
-
- EngineThread(FullRestoreEngine engine, ParcelFileDescriptor engineSocket) {
- mEngine = engine;
- engine.setRunning(true);
- // We *do* want this FileInputStream to own the underlying fd, so that
- // when we are finished with it, it closes this end of the pipe in a way
- // that signals its other end.
- mEngineStream = new FileInputStream(engineSocket.getFileDescriptor(), true);
- }
-
- public boolean isRunning() {
- return mEngine.isRunning();
- }
-
- public int waitForResult() {
- return mEngine.waitForResult();
- }
-
- @Override
- public void run() {
- try {
- while (mEngine.isRunning()) {
- // Tell it to be sure to leave the agent instance up after finishing
- mEngine.restoreOneFile(mEngineStream, false);
- }
- } finally {
- // Because mEngineStream adopted its underlying FD, this also
- // closes this end of the pipe.
- IoUtils.closeQuietly(mEngineStream);
- }
- }
-
- public void handleTimeout() {
- IoUtils.closeQuietly(mEngineStream);
- mEngine.handleTimeout();
- }
- }
-
- // state FINAL : tear everything down and we're done.
- private void finalizeRestore() {
- if (MORE_DEBUG) Slog.d(TAG, "finishing restore mObserver=" + mObserver);
-
- try {
- mTransport.finishRestore();
- } catch (Exception e) {
- Slog.e(TAG, "Error finishing restore", e);
- }
-
- // Tell the observer we're done
- if (mObserver != null) {
- try {
- mObserver.restoreFinished(mStatus);
- } catch (RemoteException e) {
- Slog.d(TAG, "Restore observer died at restoreFinished");
- }
- }
-
- // Clear any ongoing session timeout.
- mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
-
- // If we have a PM token, we must under all circumstances be sure to
- // handshake when we've finished.
- if (mPmToken > 0) {
- if (MORE_DEBUG) Slog.v(TAG, "finishing PM token " + mPmToken);
- try {
- mPackageManagerBinder.finishPackageInstall(mPmToken, mDidLaunch);
- } catch (RemoteException e) { /* can't happen */ }
- } else {
- // We were invoked via an active restore session, not by the Package
- // Manager, so start up the session timeout again.
- mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_SESSION_TIMEOUT,
- TIMEOUT_RESTORE_INTERVAL);
- }
-
- // Kick off any work that may be needed regarding app widget restores
- // TODO: http://b/22388012
- AppWidgetBackupBridge.restoreFinished(UserHandle.USER_SYSTEM);
-
- // If this was a full-system restore, record the ancestral
- // dataset information
- if (mIsSystemRestore && mPmAgent != null) {
- mAncestralPackages = mPmAgent.getRestoredPackages();
- mAncestralToken = mToken;
- writeRestoreTokens();
- }
-
- // done; we can finally release the wakelock and be legitimately done.
- Slog.i(TAG, "Restore complete.");
-
- synchronized (mPendingRestores) {
- if (mPendingRestores.size() > 0) {
- if (DEBUG) {
- Slog.d(TAG, "Starting next pending restore.");
- }
- PerformUnifiedRestoreTask task = mPendingRestores.remove();
- mBackupHandler.sendMessage(
- mBackupHandler.obtainMessage(MSG_BACKUP_RESTORE_STEP, task));
-
- } else {
- mIsRestoreInProgress = false;
- if (MORE_DEBUG) {
- Slog.d(TAG, "No pending restores.");
- }
- }
- }
-
- mWakelock.release();
- }
-
- void keyValueAgentErrorCleanup() {
- // If the agent fails restore, it might have put the app's data
- // into an incoherent state. For consistency we wipe its data
- // again in this case before continuing with normal teardown
- clearApplicationDataSynchronous(mCurrentPackage.packageName);
- keyValueAgentCleanup();
- }
-
- // TODO: clean up naming; this is now used at finish by both k/v and stream restores
- void keyValueAgentCleanup() {
- mBackupDataName.delete();
- mStageName.delete();
- try { if (mBackupData != null) mBackupData.close(); } catch (IOException e) {}
- try { if (mNewState != null) mNewState.close(); } catch (IOException e) {}
- mBackupData = mNewState = null;
-
- // if everything went okay, remember the recorded state now
- //
- // !!! TODO: the restored data could be migrated on the server
- // side into the current dataset. In that case the new state file
- // we just created would reflect the data already extant in the
- // backend, so there'd be nothing more to do. Until that happens,
- // however, we need to make sure that we record the data to the
- // current backend dataset. (Yes, this means shipping the data over
- // the wire in both directions. That's bad, but consistency comes
- // first, then efficiency.) Once we introduce server-side data
- // migration to the newly-restored device's dataset, we will change
- // the following from a discard of the newly-written state to the
- // "correct" operation of renaming into the canonical state blob.
- mNewStateName.delete(); // TODO: remove; see above comment
- //mNewStateName.renameTo(mSavedStateName); // TODO: replace with this
-
- // If this wasn't the PM pseudopackage, tear down the agent side
- if (mCurrentPackage.applicationInfo != null) {
- // unbind and tidy up even on timeout or failure
- try {
- mActivityManager.unbindBackupAgent(mCurrentPackage.applicationInfo);
-
- // The agent was probably running with a stub Application object,
- // which isn't a valid run mode for the main app logic. Shut
- // down the app so that next time it's launched, it gets the
- // usual full initialization. Note that this is only done for
- // full-system restores: when a single app has requested a restore,
- // it is explicitly not killed following that operation.
- //
- // We execute this kill when these conditions hold:
- // 1. it's not a system-uid process,
- // 2. the app did not request its own restore (mTargetPackage == null), and either
- // 3a. the app is a full-data target (TYPE_FULL_STREAM) or
- // b. the app does not state android:killAfterRestore="false" in its manifest
- final int appFlags = mCurrentPackage.applicationInfo.flags;
- final boolean killAfterRestore =
- (mCurrentPackage.applicationInfo.uid >= Process.FIRST_APPLICATION_UID)
- && ((mRestoreDescription.getDataType() == RestoreDescription.TYPE_FULL_STREAM)
- || ((appFlags & ApplicationInfo.FLAG_KILL_AFTER_RESTORE) != 0));
-
- if (mTargetPackage == null && killAfterRestore) {
- if (DEBUG) Slog.d(TAG, "Restore complete, killing host process of "
- + mCurrentPackage.applicationInfo.processName);
- mActivityManager.killApplicationProcess(
- mCurrentPackage.applicationInfo.processName,
- mCurrentPackage.applicationInfo.uid);
- }
- } catch (RemoteException e) {
- // can't happen; we run in the same process as the activity manager
- }
- }
-
- // The caller is responsible for reestablishing the state machine; our
- // responsibility here is to clear the decks for whatever comes next.
- mBackupHandler.removeMessages(MSG_RESTORE_OPERATION_TIMEOUT, this);
- }
-
- @Override
- public void operationComplete(long unusedResult) {
- removeOperation(mEphemeralOpToken);
- if (MORE_DEBUG) {
- Slog.i(TAG, "operationComplete() during restore: target="
- + mCurrentPackage.packageName
- + " state=" + mState);
- }
-
- final UnifiedRestoreState nextState;
- switch (mState) {
- case INITIAL:
- // We've just (manually) restored the PMBA. It doesn't need the
- // additional restore-finished callback so we bypass that and go
- // directly to running the queue.
- nextState = UnifiedRestoreState.RUNNING_QUEUE;
- break;
-
- case RESTORE_KEYVALUE:
- case RESTORE_FULL: {
- // Okay, we've just heard back from the agent that it's done with
- // the restore itself. We now have to send the same agent its
- // doRestoreFinished() callback, so roll into that state.
- nextState = UnifiedRestoreState.RESTORE_FINISHED;
- break;
- }
-
- case RESTORE_FINISHED: {
- // Okay, we're done with this package. Tidy up and go on to the next
- // app in the queue.
- int size = (int) mBackupDataName.length();
- EventLog.writeEvent(EventLogTags.RESTORE_PACKAGE,
- mCurrentPackage.packageName, size);
-
- // Just go back to running the restore queue
- keyValueAgentCleanup();
-
- // If there was widget state associated with this app, get the OS to
- // incorporate it into current bookeeping and then pass that along to
- // the app as part of the restore-time work.
- if (mWidgetData != null) {
- restoreWidgetData(mCurrentPackage.packageName, mWidgetData);
- }
-
- nextState = UnifiedRestoreState.RUNNING_QUEUE;
- break;
- }
-
- default: {
- // Some kind of horrible semantic error; we're in an unexpected state.
- // Back off hard and wind up.
- Slog.e(TAG, "Unexpected restore callback into state " + mState);
- keyValueAgentErrorCleanup();
- nextState = UnifiedRestoreState.FINAL;
- break;
- }
- }
-
- executeNextState(nextState);
- }
-
- // A call to agent.doRestore() or agent.doRestoreFinished() has timed out
- @Override
- public void handleCancel(boolean cancelAll) {
- removeOperation(mEphemeralOpToken);
- Slog.e(TAG, "Timeout restoring application " + mCurrentPackage.packageName);
- mMonitor = monitorEvent(mMonitor,
- BackupManagerMonitor.LOG_EVENT_ID_KEY_VALUE_RESTORE_TIMEOUT,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
- EventLog.writeEvent(EventLogTags.RESTORE_AGENT_FAILURE,
- mCurrentPackage.packageName, "restore timeout");
- // Handle like an agent that threw on invocation: wipe it and go on to the next
- keyValueAgentErrorCleanup();
- executeNextState(UnifiedRestoreState.RUNNING_QUEUE);
- }
-
- void executeNextState(UnifiedRestoreState nextState) {
- if (MORE_DEBUG) Slog.i(TAG, " => executing next step on "
- + this + " nextState=" + nextState);
- mState = nextState;
- Message msg = mBackupHandler.obtainMessage(MSG_BACKUP_RESTORE_STEP, this);
- mBackupHandler.sendMessage(msg);
- }
-
- // restore observer support
- void sendStartRestore(int numPackages) {
- if (mObserver != null) {
- try {
- mObserver.restoreStarting(numPackages);
- } catch (RemoteException e) {
- Slog.w(TAG, "Restore observer went away: startRestore");
- mObserver = null;
- }
- }
- }
-
- void sendOnRestorePackage(String name) {
- if (mObserver != null) {
- if (mObserver != null) {
- try {
- mObserver.onUpdate(mCount, name);
- } catch (RemoteException e) {
- Slog.d(TAG, "Restore observer died in onUpdate");
- mObserver = null;
- }
- }
- }
- }
-
- void sendEndRestore() {
- if (mObserver != null) {
- try {
- mObserver.restoreFinished(mStatus);
- } catch (RemoteException e) {
- Slog.w(TAG, "Restore observer went away: endRestore");
- mObserver = null;
- }
- }
- }
- }
-
- class PerformClearTask implements Runnable {
- IBackupTransport mTransport;
- PackageInfo mPackage;
-
- PerformClearTask(IBackupTransport transport, PackageInfo packageInfo) {
- mTransport = transport;
- mPackage = packageInfo;
- }
-
- public void run() {
- try {
- // Clear the on-device backup state to ensure a full backup next time
- File stateDir = new File(mBaseStateDir, mTransport.transportDirName());
- File stateFile = new File(stateDir, mPackage.packageName);
- stateFile.delete();
-
- // Tell the transport to remove all the persistent storage for the app
- // TODO - need to handle failures
- mTransport.clearBackupData(mPackage);
- } catch (Exception e) {
- Slog.e(TAG, "Transport threw clearing data for " + mPackage + ": " + e.getMessage());
- } finally {
- try {
- // TODO - need to handle failures
- mTransport.finishBackup();
- } catch (Exception e) {
- // Nothing we can do here, alas
- Slog.e(TAG, "Unable to mark clear operation finished: " + e.getMessage());
- }
-
- // Last but not least, release the cpu
- mWakelock.release();
- }
- }
- }
-
- class PerformInitializeTask implements Runnable {
- String[] mQueue;
- IBackupObserver mObserver;
-
- PerformInitializeTask(String[] transportNames, IBackupObserver observer) {
- mQueue = transportNames;
- mObserver = observer;
- }
-
- private void notifyResult(String target, int status) {
- try {
- if (mObserver != null) {
- mObserver.onResult(target, status);
- }
- } catch (RemoteException ignored) {
- mObserver = null; // don't try again
- }
- }
-
- private void notifyFinished(int status) {
- try {
- if (mObserver != null) {
- mObserver.backupFinished(status);
- }
- } catch (RemoteException ignored) {
- mObserver = null;
- }
- }
-
- public void run() {
- // mWakelock is *acquired* when execution begins here
- int result = BackupTransport.TRANSPORT_OK;
- try {
- for (String transportName : mQueue) {
- IBackupTransport transport =
- mTransportManager.getTransportBinder(transportName);
- if (transport == null) {
- Slog.e(TAG, "Requested init for " + transportName + " but not found");
- continue;
- }
-
- Slog.i(TAG, "Initializing (wiping) backup transport storage: " + transportName);
- String transportDirName = transport.transportDirName();
- EventLog.writeEvent(EventLogTags.BACKUP_START, transportDirName);
- long startRealtime = SystemClock.elapsedRealtime();
- int status = transport.initializeDevice();
-
- if (status == BackupTransport.TRANSPORT_OK) {
- status = transport.finishBackup();
- }
-
- // Okay, the wipe really happened. Clean up our local bookkeeping.
- if (status == BackupTransport.TRANSPORT_OK) {
- Slog.i(TAG, "Device init successful");
- int millis = (int) (SystemClock.elapsedRealtime() - startRealtime);
- EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE);
- resetBackupState(new File(mBaseStateDir, transportDirName));
- EventLog.writeEvent(EventLogTags.BACKUP_SUCCESS, 0, millis);
- synchronized (mQueueLock) {
- recordInitPendingLocked(false, transportName);
- }
- notifyResult(transportName, BackupTransport.TRANSPORT_OK);
- } else {
- // If this didn't work, requeue this one and try again
- // after a suitable interval
- Slog.e(TAG, "Transport error in initializeDevice()");
- EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)");
- synchronized (mQueueLock) {
- recordInitPendingLocked(true, transportName);
- }
- notifyResult(transportName, status);
- result = status;
-
- // do this via another alarm to make sure of the wakelock states
- long delay = transport.requestBackupTime();
- Slog.w(TAG, "Init failed on " + transportName + " resched in " + delay);
- mAlarmManager.set(AlarmManager.RTC_WAKEUP,
- System.currentTimeMillis() + delay, mRunInitIntent);
- }
- }
- } catch (Exception e) {
- Slog.e(TAG, "Unexpected error performing init", e);
- result = BackupTransport.TRANSPORT_ERROR;
- } finally {
- // Done; release the wakelock
- notifyFinished(result);
- mWakelock.release();
- }
- }
- }
-
- private void dataChangedImpl(String packageName) {
- HashSet<String> targets = dataChangedTargets(packageName);
- dataChangedImpl(packageName, targets);
- }
-
- private void dataChangedImpl(String packageName, HashSet<String> targets) {
- // Record that we need a backup pass for the caller. Since multiple callers
- // may share a uid, we need to note all candidates within that uid and schedule
- // a backup pass for each of them.
- if (targets == null) {
- Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'"
- + " uid=" + Binder.getCallingUid());
- return;
- }
-
- synchronized (mQueueLock) {
- // Note that this client has made data changes that need to be backed up
- if (targets.contains(packageName)) {
- // Add the caller to the set of pending backups. If there is
- // one already there, then overwrite it, but no harm done.
- BackupRequest req = new BackupRequest(packageName);
- if (mPendingBackups.put(packageName, req) == null) {
- if (MORE_DEBUG) Slog.d(TAG, "Now staging backup of " + packageName);
-
- // Journal this request in case of crash. The put()
- // operation returned null when this package was not already
- // in the set; we want to avoid touching the disk redundantly.
- writeToJournalLocked(packageName);
- }
- }
- }
-
- // ...and schedule a backup pass if necessary
- KeyValueBackupJob.schedule(mContext, mConstants);
- }
-
- // Note: packageName is currently unused, but may be in the future
- private HashSet<String> dataChangedTargets(String packageName) {
- // If the caller does not hold the BACKUP permission, it can only request a
- // backup of its own data.
- if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(),
- Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) {
- synchronized (mBackupParticipants) {
- return mBackupParticipants.get(Binder.getCallingUid());
- }
- }
-
- // a caller with full permission can ask to back up any participating app
- HashSet<String> targets = new HashSet<String>();
- if (PACKAGE_MANAGER_SENTINEL.equals(packageName)) {
- targets.add(PACKAGE_MANAGER_SENTINEL);
- } else {
- synchronized (mBackupParticipants) {
- int N = mBackupParticipants.size();
- for (int i = 0; i < N; i++) {
- HashSet<String> s = mBackupParticipants.valueAt(i);
- if (s != null) {
- targets.addAll(s);
- }
- }
- }
- }
- return targets;
- }
-
- private void writeToJournalLocked(String str) {
- RandomAccessFile out = null;
- try {
- if (mJournal == null) mJournal = File.createTempFile("journal", null, mJournalDir);
- out = new RandomAccessFile(mJournal, "rws");
- out.seek(out.length());
- out.writeUTF(str);
- } catch (IOException e) {
- Slog.e(TAG, "Can't write " + str + " to backup journal", e);
- mJournal = null;
- } finally {
- try { if (out != null) out.close(); } catch (IOException e) {}
- }
- }
-
- // ----- IBackupManager binder interface -----
-
- @Override
- public void dataChanged(final String packageName) {
- final int callingUserHandle = UserHandle.getCallingUserId();
- if (callingUserHandle != UserHandle.USER_SYSTEM) {
- // TODO: http://b/22388012
- // App is running under a non-owner user profile. For now, we do not back
- // up data from secondary user profiles.
- // TODO: backups for all user profiles although don't add backup for profiles
- // without adding admin control in DevicePolicyManager.
- if (MORE_DEBUG) {
- Slog.v(TAG, "dataChanged(" + packageName + ") ignored because it's user "
- + callingUserHandle);
- }
- return;
- }
-
- final HashSet<String> targets = dataChangedTargets(packageName);
- if (targets == null) {
- Slog.w(TAG, "dataChanged but no participant pkg='" + packageName + "'"
- + " uid=" + Binder.getCallingUid());
- return;
- }
-
- mBackupHandler.post(new Runnable() {
- public void run() {
- dataChangedImpl(packageName, targets);
- }
- });
- }
-
- // Run an initialize operation for the given transport
- @Override
- public void initializeTransports(String[] transportNames, IBackupObserver observer) {
- mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "initializeTransport");
- if (MORE_DEBUG) {
- Slog.v(TAG, "initializeTransports() of " + transportNames);
- }
-
- final long oldId = Binder.clearCallingIdentity();
- try {
- mWakelock.acquire();
- mBackupHandler.post(new PerformInitializeTask(transportNames, observer));
- } finally {
- Binder.restoreCallingIdentity(oldId);
- }
- }
-
- // Clear the given package's backup data from the current transport
- @Override
- public void clearBackupData(String transportName, String packageName) {
- if (DEBUG) Slog.v(TAG, "clearBackupData() of " + packageName + " on " + transportName);
- PackageInfo info;
- try {
- info = mPackageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
- } catch (NameNotFoundException e) {
- Slog.d(TAG, "No such package '" + packageName + "' - not clearing backup data");
- return;
- }
-
- // If the caller does not hold the BACKUP permission, it can only request a
- // wipe of its own backed-up data.
- HashSet<String> apps;
- if ((mContext.checkPermission(android.Manifest.permission.BACKUP, Binder.getCallingPid(),
- Binder.getCallingUid())) == PackageManager.PERMISSION_DENIED) {
- apps = mBackupParticipants.get(Binder.getCallingUid());
- } else {
- // a caller with full permission can ask to back up any participating app
- // !!! TODO: allow data-clear of ANY app?
- if (MORE_DEBUG) Slog.v(TAG, "Privileged caller, allowing clear of other apps");
- apps = new HashSet<String>();
- int N = mBackupParticipants.size();
- for (int i = 0; i < N; i++) {
- HashSet<String> s = mBackupParticipants.valueAt(i);
- if (s != null) {
- apps.addAll(s);
- }
- }
- }
-
- // Is the given app an available participant?
- if (apps.contains(packageName)) {
- // found it; fire off the clear request
- if (MORE_DEBUG) Slog.v(TAG, "Found the app - running clear process");
- mBackupHandler.removeMessages(MSG_RETRY_CLEAR);
- synchronized (mQueueLock) {
- final IBackupTransport transport =
- mTransportManager.getTransportBinder(transportName);
- if (transport == null) {
- // transport is currently unavailable -- make sure to retry
- Message msg = mBackupHandler.obtainMessage(MSG_RETRY_CLEAR,
- new ClearRetryParams(transportName, packageName));
- mBackupHandler.sendMessageDelayed(msg, TRANSPORT_RETRY_INTERVAL);
- return;
- }
- long oldId = Binder.clearCallingIdentity();
- mWakelock.acquire();
- Message msg = mBackupHandler.obtainMessage(MSG_RUN_CLEAR,
- new ClearParams(transport, info));
- mBackupHandler.sendMessage(msg);
- Binder.restoreCallingIdentity(oldId);
- }
- }
- }
-
- // Run a backup pass immediately for any applications that have declared
- // that they have pending updates.
- @Override
- public void backupNow() {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "backupNow");
-
- final PowerSaveState result =
- mPowerManager.getPowerSaveState(ServiceType.KEYVALUE_BACKUP);
- if (result.batterySaverEnabled) {
- if (DEBUG) Slog.v(TAG, "Not running backup while in battery save mode");
- KeyValueBackupJob.schedule(mContext, mConstants); // try again in several hours
- } else {
- if (DEBUG) Slog.v(TAG, "Scheduling immediate backup pass");
- synchronized (mQueueLock) {
- // Fire the intent that kicks off the whole shebang...
- try {
- mRunBackupIntent.send();
- } catch (PendingIntent.CanceledException e) {
- // should never happen
- Slog.e(TAG, "run-backup intent cancelled!");
- }
-
- // ...and cancel any pending scheduled job, because we've just superseded it
- KeyValueBackupJob.cancel(mContext);
- }
- }
- }
-
- boolean deviceIsProvisioned() {
- final ContentResolver resolver = mContext.getContentResolver();
- return (Settings.Global.getInt(resolver, Settings.Global.DEVICE_PROVISIONED, 0) != 0);
- }
-
- // Run a backup pass for the given packages, writing the resulting data stream
- // to the supplied file descriptor. This method is synchronous and does not return
- // to the caller until the backup has been completed.
- //
- // This is the variant used by 'adb backup'; it requires on-screen confirmation
- // by the user because it can be used to offload data over untrusted USB.
- @Override
- public void adbBackup(ParcelFileDescriptor fd, boolean includeApks, boolean includeObbs,
- boolean includeShared, boolean doWidgets, boolean doAllApps, boolean includeSystem,
- boolean compress, boolean doKeyValue, String[] pkgList) {
- mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "adbBackup");
-
- final int callingUserHandle = UserHandle.getCallingUserId();
- // TODO: http://b/22388012
- if (callingUserHandle != UserHandle.USER_SYSTEM) {
- throw new IllegalStateException("Backup supported only for the device owner");
- }
-
- // Validate
- if (!doAllApps) {
- if (!includeShared) {
- // If we're backing up shared data (sdcard or equivalent), then we can run
- // without any supplied app names. Otherwise, we'd be doing no work, so
- // report the error.
- if (pkgList == null || pkgList.length == 0) {
- throw new IllegalArgumentException(
- "Backup requested but neither shared nor any apps named");
- }
- }
- }
-
- long oldId = Binder.clearCallingIdentity();
- try {
- // Doesn't make sense to do a full backup prior to setup
- if (!deviceIsProvisioned()) {
- Slog.i(TAG, "Backup not supported before setup");
- return;
- }
-
- if (DEBUG) Slog.v(TAG, "Requesting backup: apks=" + includeApks + " obb=" + includeObbs
- + " shared=" + includeShared + " all=" + doAllApps + " system="
- + includeSystem + " includekeyvalue=" + doKeyValue + " pkgs=" + pkgList);
- Slog.i(TAG, "Beginning adb backup...");
-
- AdbBackupParams params = new AdbBackupParams(fd, includeApks, includeObbs,
- includeShared, doWidgets, doAllApps, includeSystem, compress, doKeyValue,
- pkgList);
- final int token = generateRandomIntegerToken();
- synchronized (mAdbBackupRestoreConfirmations) {
- mAdbBackupRestoreConfirmations.put(token, params);
- }
-
- // start up the confirmation UI
- if (DEBUG) Slog.d(TAG, "Starting backup confirmation UI, token=" + token);
- if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) {
- Slog.e(TAG, "Unable to launch backup confirmation UI");
- mAdbBackupRestoreConfirmations.delete(token);
- return;
- }
-
- // make sure the screen is lit for the user interaction
- mPowerManager.userActivity(SystemClock.uptimeMillis(),
- PowerManager.USER_ACTIVITY_EVENT_OTHER,
- 0);
-
- // start the confirmation countdown
- startConfirmationTimeout(token, params);
-
- // wait for the backup to be performed
- if (DEBUG) Slog.d(TAG, "Waiting for backup completion...");
- waitForCompletion(params);
- } finally {
- try {
- fd.close();
- } catch (IOException e) {
- Slog.e(TAG, "IO error closing output for adb backup: " + e.getMessage());
- }
- Binder.restoreCallingIdentity(oldId);
- Slog.d(TAG, "Adb backup processing complete.");
- }
- }
-
- @Override
- public void fullTransportBackup(String[] pkgNames) {
- mContext.enforceCallingPermission(android.Manifest.permission.BACKUP,
- "fullTransportBackup");
-
- final int callingUserHandle = UserHandle.getCallingUserId();
- // TODO: http://b/22388012
- if (callingUserHandle != UserHandle.USER_SYSTEM) {
- throw new IllegalStateException("Restore supported only for the device owner");
- }
-
- if (!fullBackupAllowable(mTransportManager.getCurrentTransportBinder())) {
- Slog.i(TAG, "Full backup not currently possible -- key/value backup not yet run?");
- } else {
- if (DEBUG) {
- Slog.d(TAG, "fullTransportBackup()");
- }
-
- final long oldId = Binder.clearCallingIdentity();
- try {
- CountDownLatch latch = new CountDownLatch(1);
- PerformFullTransportBackupTask task = new PerformFullTransportBackupTask(null,
- pkgNames, false, null, latch, null, null, false /* userInitiated */);
- // Acquiring wakelock for PerformFullTransportBackupTask before its start.
- mWakelock.acquire();
- (new Thread(task, "full-transport-master")).start();
- do {
- try {
- latch.await();
- break;
- } catch (InterruptedException e) {
- // Just go back to waiting for the latch to indicate completion
- }
- } while (true);
-
- // We just ran a backup on these packages, so kick them to the end of the queue
- final long now = System.currentTimeMillis();
- for (String pkg : pkgNames) {
- enqueueFullBackup(pkg, now);
- }
- } finally {
- Binder.restoreCallingIdentity(oldId);
- }
- }
-
- if (DEBUG) {
- Slog.d(TAG, "Done with full transport backup.");
- }
- }
-
- @Override
- public void adbRestore(ParcelFileDescriptor fd) {
- mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "adbRestore");
-
- final int callingUserHandle = UserHandle.getCallingUserId();
- // TODO: http://b/22388012
- if (callingUserHandle != UserHandle.USER_SYSTEM) {
- throw new IllegalStateException("Restore supported only for the device owner");
- }
-
- long oldId = Binder.clearCallingIdentity();
-
- try {
- // Check whether the device has been provisioned -- we don't handle
- // full restores prior to completing the setup process.
- if (!deviceIsProvisioned()) {
- Slog.i(TAG, "Full restore not permitted before setup");
- return;
- }
-
- Slog.i(TAG, "Beginning restore...");
-
- AdbRestoreParams params = new AdbRestoreParams(fd);
- final int token = generateRandomIntegerToken();
- synchronized (mAdbBackupRestoreConfirmations) {
- mAdbBackupRestoreConfirmations.put(token, params);
- }
-
- // start up the confirmation UI
- if (DEBUG) Slog.d(TAG, "Starting restore confirmation UI, token=" + token);
- if (!startConfirmationUi(token, FullBackup.FULL_RESTORE_INTENT_ACTION)) {
- Slog.e(TAG, "Unable to launch restore confirmation");
- mAdbBackupRestoreConfirmations.delete(token);
- return;
- }
-
- // make sure the screen is lit for the user interaction
- mPowerManager.userActivity(SystemClock.uptimeMillis(),
- PowerManager.USER_ACTIVITY_EVENT_OTHER,
- 0);
-
- // start the confirmation countdown
- startConfirmationTimeout(token, params);
-
- // wait for the restore to be performed
- if (DEBUG) Slog.d(TAG, "Waiting for restore completion...");
- waitForCompletion(params);
- } finally {
- try {
- fd.close();
- } catch (IOException e) {
- Slog.w(TAG, "Error trying to close fd after adb restore: " + e);
- }
- Binder.restoreCallingIdentity(oldId);
- Slog.i(TAG, "adb restore processing complete.");
- }
- }
-
- boolean startConfirmationUi(int token, String action) {
- try {
- Intent confIntent = new Intent(action);
- confIntent.setClassName("com.android.backupconfirm",
- "com.android.backupconfirm.BackupRestoreConfirmation");
- confIntent.putExtra(FullBackup.CONF_TOKEN_INTENT_EXTRA, token);
- confIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mContext.startActivityAsUser(confIntent, UserHandle.SYSTEM);
- } catch (ActivityNotFoundException e) {
- return false;
- }
- return true;
- }
-
- void startConfirmationTimeout(int token, AdbParams params) {
- if (MORE_DEBUG) Slog.d(TAG, "Posting conf timeout msg after "
- + TIMEOUT_FULL_CONFIRMATION + " millis");
- Message msg = mBackupHandler.obtainMessage(MSG_FULL_CONFIRMATION_TIMEOUT,
- token, 0, params);
- mBackupHandler.sendMessageDelayed(msg, TIMEOUT_FULL_CONFIRMATION);
- }
-
- void waitForCompletion(AdbParams params) {
- synchronized (params.latch) {
- while (params.latch.get() == false) {
- try {
- params.latch.wait();
- } catch (InterruptedException e) { /* never interrupted */ }
- }
- }
- }
-
- void signalAdbBackupRestoreCompletion(AdbParams params) {
- synchronized (params.latch) {
- params.latch.set(true);
- params.latch.notifyAll();
- }
- }
-
- // Confirm that the previously-requested full backup/restore operation can proceed. This
- // is used to require a user-facing disclosure about the operation.
- @Override
- public void acknowledgeAdbBackupOrRestore(int token, boolean allow,
- String curPassword, String encPpassword, IFullBackupRestoreObserver observer) {
- if (DEBUG) Slog.d(TAG, "acknowledgeAdbBackupOrRestore : token=" + token
- + " allow=" + allow);
-
- // TODO: possibly require not just this signature-only permission, but even
- // require that the specific designated confirmation-UI app uid is the caller?
- mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "acknowledgeAdbBackupOrRestore");
-
- long oldId = Binder.clearCallingIdentity();
- try {
-
- AdbParams params;
- synchronized (mAdbBackupRestoreConfirmations) {
- params = mAdbBackupRestoreConfirmations.get(token);
- if (params != null) {
- mBackupHandler.removeMessages(MSG_FULL_CONFIRMATION_TIMEOUT, params);
- mAdbBackupRestoreConfirmations.delete(token);
-
- if (allow) {
- final int verb = params instanceof AdbBackupParams
- ? MSG_RUN_ADB_BACKUP
- : MSG_RUN_ADB_RESTORE;
-
- params.observer = observer;
- params.curPassword = curPassword;
-
- params.encryptPassword = encPpassword;
-
- if (MORE_DEBUG) Slog.d(TAG, "Sending conf message with verb " + verb);
- mWakelock.acquire();
- Message msg = mBackupHandler.obtainMessage(verb, params);
- mBackupHandler.sendMessage(msg);
- } else {
- Slog.w(TAG, "User rejected full backup/restore operation");
- // indicate completion without having actually transferred any data
- signalAdbBackupRestoreCompletion(params);
- }
- } else {
- Slog.w(TAG, "Attempted to ack full backup/restore with invalid token");
- }
- }
- } finally {
- Binder.restoreCallingIdentity(oldId);
- }
- }
-
- private static boolean backupSettingMigrated(int userId) {
- File base = new File(Environment.getDataDirectory(), "backup");
- File enableFile = new File(base, BACKUP_ENABLE_FILE);
- return enableFile.exists();
- }
-
- private static boolean readBackupEnableState(int userId) {
- File base = new File(Environment.getDataDirectory(), "backup");
- File enableFile = new File(base, BACKUP_ENABLE_FILE);
- if (enableFile.exists()) {
- try (FileInputStream fin = new FileInputStream(enableFile)) {
- int state = fin.read();
- return state != 0;
- } catch (IOException e) {
- // can't read the file; fall through to assume disabled
- Slog.e(TAG, "Cannot read enable state; assuming disabled");
- }
- } else {
- if (DEBUG) {
- Slog.i(TAG, "isBackupEnabled() => false due to absent settings file");
- }
- }
- return false;
- }
-
- private static void writeBackupEnableState(boolean enable, int userId) {
- File base = new File(Environment.getDataDirectory(), "backup");
- File enableFile = new File(base, BACKUP_ENABLE_FILE);
- File stage = new File(base, BACKUP_ENABLE_FILE + "-stage");
- FileOutputStream fout = null;
- try {
- fout = new FileOutputStream(stage);
- fout.write(enable ? 1 : 0);
- fout.close();
- stage.renameTo(enableFile);
- // will be synced immediately by the try-with-resources call to close()
- } catch (IOException|RuntimeException e) {
- // Whoops; looks like we're doomed. Roll everything out, disabled,
- // including the legacy state.
- Slog.e(TAG, "Unable to record backup enable state; reverting to disabled: "
- + e.getMessage());
-
- final ContentResolver r = sInstance.mContext.getContentResolver();
- Settings.Secure.putStringForUser(r,
- Settings.Secure.BACKUP_ENABLED, null, userId);
- enableFile.delete();
- stage.delete();
- } finally {
- IoUtils.closeQuietly(fout);
- }
- }
-
- // Enable/disable backups
- @Override
- public void setBackupEnabled(boolean enable) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "setBackupEnabled");
-
- Slog.i(TAG, "Backup enabled => " + enable);
-
- long oldId = Binder.clearCallingIdentity();
- try {
- boolean wasEnabled = mEnabled;
- synchronized (this) {
- writeBackupEnableState(enable, UserHandle.USER_SYSTEM);
- mEnabled = enable;
- }
-
- synchronized (mQueueLock) {
- if (enable && !wasEnabled && mProvisioned) {
- // if we've just been enabled, start scheduling backup passes
- KeyValueBackupJob.schedule(mContext, mConstants);
- scheduleNextFullBackupJob(0);
- } else if (!enable) {
- // No longer enabled, so stop running backups
- if (MORE_DEBUG) Slog.i(TAG, "Opting out of backup");
-
- KeyValueBackupJob.cancel(mContext);
-
- // This also constitutes an opt-out, so we wipe any data for
- // this device from the backend. We start that process with
- // an alarm in order to guarantee wakelock states.
- if (wasEnabled && mProvisioned) {
- // NOTE: we currently flush every registered transport, not just
- // the currently-active one.
- String[] allTransports = mTransportManager.getBoundTransportNames();
- // build the set of transports for which we are posting an init
- for (String transport : allTransports) {
- recordInitPendingLocked(true, transport);
- }
- mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
- mRunInitIntent);
- }
- }
- }
- } finally {
- Binder.restoreCallingIdentity(oldId);
- }
- }
-
- // Enable/disable automatic restore of app data at install time
- @Override
- public void setAutoRestore(boolean doAutoRestore) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "setAutoRestore");
-
- Slog.i(TAG, "Auto restore => " + doAutoRestore);
-
- final long oldId = Binder.clearCallingIdentity();
- try {
- synchronized (this) {
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.BACKUP_AUTO_RESTORE, doAutoRestore ? 1 : 0);
- mAutoRestore = doAutoRestore;
- }
- } finally {
- Binder.restoreCallingIdentity(oldId);
- }
- }
-
- // Mark the backup service as having been provisioned
- @Override
- public void setBackupProvisioned(boolean available) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "setBackupProvisioned");
- /*
- * This is now a no-op; provisioning is simply the device's own setup state.
- */
- }
-
- // Report whether the backup mechanism is currently enabled
- @Override
- public boolean isBackupEnabled() {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "isBackupEnabled");
- return mEnabled; // no need to synchronize just to read it
- }
-
- // Report the name of the currently active transport
- @Override
- public String getCurrentTransport() {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "getCurrentTransport");
- String currentTransport = mTransportManager.getCurrentTransportName();
- if (MORE_DEBUG) Slog.v(TAG, "... getCurrentTransport() returning " + currentTransport);
- return currentTransport;
- }
-
- // Report all known, available backup transports
- @Override
- public String[] listAllTransports() {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "listAllTransports");
-
- return mTransportManager.getBoundTransportNames();
- }
-
- @Override
- public ComponentName[] listAllTransportComponents() {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "listAllTransportComponents");
- return mTransportManager.getAllTransportComponents();
- }
-
- @Override
- public String[] getTransportWhitelist() {
- // No permission check, intentionally.
- Set<ComponentName> whitelistedComponents = mTransportManager.getTransportWhitelist();
- String[] whitelistedTransports = new String[whitelistedComponents.size()];
- int i = 0;
- for (ComponentName component : whitelistedComponents) {
- whitelistedTransports[i] = component.flattenToShortString();
- i++;
- }
- return whitelistedTransports;
- }
-
- // Select which transport to use for the next backup operation.
- @Override
- public String selectBackupTransport(String transport) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "selectBackupTransport");
-
- final long oldId = Binder.clearCallingIdentity();
- try {
- String prevTransport = mTransportManager.selectTransport(transport);
- updateStateForTransport(transport);
- Slog.v(TAG, "selectBackupTransport() set " + mTransportManager.getCurrentTransportName()
- + " returning " + prevTransport);
- return prevTransport;
- } finally {
- Binder.restoreCallingIdentity(oldId);
- }
- }
-
- @Override
- public void selectBackupTransportAsync(final ComponentName transport,
- final ISelectBackupTransportCallback listener) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "selectBackupTransportAsync");
-
- final long oldId = Binder.clearCallingIdentity();
-
- Slog.v(TAG, "selectBackupTransportAsync() called with transport " +
- transport.flattenToShortString());
-
- mTransportManager.ensureTransportReady(transport, new TransportManager.TransportReadyCallback() {
- @Override
- public void onSuccess(String transportName) {
- mTransportManager.selectTransport(transportName);
- updateStateForTransport(mTransportManager.getCurrentTransportName());
- Slog.v(TAG, "Transport successfully selected: " + transport.flattenToShortString());
- try {
- listener.onSuccess(transportName);
- } catch (RemoteException e) {
- // Nothing to do here.
- }
- }
-
- @Override
- public void onFailure(int reason) {
- Slog.v(TAG, "Failed to select transport: " + transport.flattenToShortString());
- try {
- listener.onFailure(reason);
- } catch (RemoteException e) {
- // Nothing to do here.
- }
- }
- });
-
- Binder.restoreCallingIdentity(oldId);
- }
-
- private void updateStateForTransport(String newTransportName) {
- // Publish the name change
- Settings.Secure.putString(mContext.getContentResolver(),
- Settings.Secure.BACKUP_TRANSPORT, newTransportName);
-
- // And update our current-dataset bookkeeping
- IBackupTransport transport = mTransportManager.getTransportBinder(newTransportName);
- if (transport != null) {
- try {
- mCurrentToken = transport.getCurrentRestoreSet();
- } catch (Exception e) {
- // Oops. We can't know the current dataset token, so reset and figure it out
- // when we do the next k/v backup operation on this transport.
- mCurrentToken = 0;
- }
- } else {
- // The named transport isn't bound at this particular moment, so we can't
- // know yet what its current dataset token is. Reset as above.
- mCurrentToken = 0;
- }
- }
-
- // Supply the configuration Intent for the given transport. If the name is not one
- // of the available transports, or if the transport does not supply any configuration
- // UI, the method returns null.
- @Override
- public Intent getConfigurationIntent(String transportName) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "getConfigurationIntent");
-
- final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
- if (transport != null) {
- try {
- final Intent intent = transport.configurationIntent();
- if (MORE_DEBUG) Slog.d(TAG, "getConfigurationIntent() returning config intent "
- + intent);
- return intent;
- } catch (Exception e) {
- /* fall through to return null */
- Slog.e(TAG, "Unable to get configuration intent from transport: " + e.getMessage());
- }
- }
-
- return null;
- }
-
- // Supply the configuration summary string for the given transport. If the name is
- // not one of the available transports, or if the transport does not supply any
- // summary / destination string, the method can return null.
- //
- // This string is used VERBATIM as the summary text of the relevant Settings item!
- @Override
- public String getDestinationString(String transportName) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "getDestinationString");
-
- final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
- if (transport != null) {
- try {
- final String text = transport.currentDestinationString();
- if (MORE_DEBUG) Slog.d(TAG, "getDestinationString() returning " + text);
- return text;
- } catch (Exception e) {
- /* fall through to return null */
- Slog.e(TAG, "Unable to get string from transport: " + e.getMessage());
- }
- }
-
- return null;
- }
-
- // Supply the manage-data intent for the given transport.
- @Override
- public Intent getDataManagementIntent(String transportName) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "getDataManagementIntent");
-
- final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
- if (transport != null) {
- try {
- final Intent intent = transport.dataManagementIntent();
- if (MORE_DEBUG) Slog.d(TAG, "getDataManagementIntent() returning intent "
- + intent);
- return intent;
- } catch (Exception e) {
- /* fall through to return null */
- Slog.e(TAG, "Unable to get management intent from transport: " + e.getMessage());
- }
- }
-
- return null;
- }
-
- // Supply the menu label for affordances that fire the manage-data intent
- // for the given transport.
- @Override
- public String getDataManagementLabel(String transportName) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "getDataManagementLabel");
-
- final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
- if (transport != null) {
- try {
- final String text = transport.dataManagementLabel();
- if (MORE_DEBUG) Slog.d(TAG, "getDataManagementLabel() returning " + text);
- return text;
- } catch (Exception e) {
- /* fall through to return null */
- Slog.e(TAG, "Unable to get management label from transport: " + e.getMessage());
- }
- }
-
- return null;
- }
-
- // Callback: a requested backup agent has been instantiated. This should only
- // be called from the Activity Manager.
- @Override
- public void agentConnected(String packageName, IBinder agentBinder) {
- synchronized(mAgentConnectLock) {
- if (Binder.getCallingUid() == Process.SYSTEM_UID) {
- Slog.d(TAG, "agentConnected pkg=" + packageName + " agent=" + agentBinder);
- IBackupAgent agent = IBackupAgent.Stub.asInterface(agentBinder);
- mConnectedAgent = agent;
- mConnecting = false;
- } else {
- Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid()
- + " claiming agent connected");
- }
- mAgentConnectLock.notifyAll();
- }
- }
-
- // Callback: a backup agent has failed to come up, or has unexpectedly quit.
- // If the agent failed to come up in the first place, the agentBinder argument
- // will be null. This should only be called from the Activity Manager.
- @Override
- public void agentDisconnected(String packageName) {
- // TODO: handle backup being interrupted
- synchronized(mAgentConnectLock) {
- if (Binder.getCallingUid() == Process.SYSTEM_UID) {
- mConnectedAgent = null;
- mConnecting = false;
- } else {
- Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid()
- + " claiming agent disconnected");
- }
- mAgentConnectLock.notifyAll();
- }
- }
-
- // An application being installed will need a restore pass, then the Package Manager
- // will need to be told when the restore is finished.
- @Override
- public void restoreAtInstall(String packageName, int token) {
- if (Binder.getCallingUid() != Process.SYSTEM_UID) {
- Slog.w(TAG, "Non-system process uid=" + Binder.getCallingUid()
- + " attemping install-time restore");
- return;
- }
-
- boolean skip = false;
-
- long restoreSet = getAvailableRestoreToken(packageName);
- if (DEBUG) Slog.v(TAG, "restoreAtInstall pkg=" + packageName
- + " token=" + Integer.toHexString(token)
- + " restoreSet=" + Long.toHexString(restoreSet));
- if (restoreSet == 0) {
- if (MORE_DEBUG) Slog.i(TAG, "No restore set");
- skip = true;
- }
-
- // Do we have a transport to fetch data for us?
- IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
- if (transport == null) {
- if (DEBUG) Slog.w(TAG, "No transport");
- skip = true;
- }
-
- if (!mAutoRestore) {
- if (DEBUG) {
- Slog.w(TAG, "Non-restorable state: auto=" + mAutoRestore);
- }
- skip = true;
- }
-
- if (!skip) {
- try {
- // okay, we're going to attempt a restore of this package from this restore set.
- // The eventual message back into the Package Manager to run the post-install
- // steps for 'token' will be issued from the restore handling code.
-
- // This can throw and so *must* happen before the wakelock is acquired
- String dirName = transport.transportDirName();
-
- mWakelock.acquire();
- if (MORE_DEBUG) {
- Slog.d(TAG, "Restore at install of " + packageName);
- }
- Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE);
- msg.obj = new RestoreParams(transport, dirName, null, null,
- restoreSet, packageName, token);
- mBackupHandler.sendMessage(msg);
- } catch (Exception e) {
- // Calling into the transport broke; back off and proceed with the installation.
- Slog.e(TAG, "Unable to contact transport: " + e.getMessage());
- skip = true;
- }
- }
-
- if (skip) {
- // Auto-restore disabled or no way to attempt a restore; just tell the Package
- // Manager to proceed with the post-install handling for this package.
- if (DEBUG) Slog.v(TAG, "Finishing install immediately");
- try {
- mPackageManagerBinder.finishPackageInstall(token, false);
- } catch (RemoteException e) { /* can't happen */ }
- }
- }
-
- // Hand off a restore session
- @Override
- public IRestoreSession beginRestoreSession(String packageName, String transport) {
- if (DEBUG) Slog.v(TAG, "beginRestoreSession: pkg=" + packageName
- + " transport=" + transport);
-
- boolean needPermission = true;
- if (transport == null) {
- transport = mTransportManager.getCurrentTransportName();
-
- if (packageName != null) {
- PackageInfo app = null;
- try {
- app = mPackageManager.getPackageInfo(packageName, 0);
- } catch (NameNotFoundException nnf) {
- Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName);
- throw new IllegalArgumentException("Package " + packageName + " not found");
- }
-
- if (app.applicationInfo.uid == Binder.getCallingUid()) {
- // So: using the current active transport, and the caller has asked
- // that its own package will be restored. In this narrow use case
- // we do not require the caller to hold the permission.
- needPermission = false;
- }
- }
- }
-
- if (needPermission) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "beginRestoreSession");
- } else {
- if (DEBUG) Slog.d(TAG, "restoring self on current transport; no permission needed");
- }
-
- synchronized(this) {
- if (mActiveRestoreSession != null) {
- Slog.i(TAG, "Restore session requested but one already active");
- return null;
- }
- if (mBackupRunning) {
- Slog.i(TAG, "Restore session requested but currently running backups");
- return null;
- }
- mActiveRestoreSession = new ActiveRestoreSession(packageName, transport);
- mBackupHandler.sendEmptyMessageDelayed(MSG_RESTORE_SESSION_TIMEOUT,
- TIMEOUT_RESTORE_INTERVAL);
- }
- return mActiveRestoreSession;
- }
-
- void clearRestoreSession(ActiveRestoreSession currentSession) {
- synchronized(this) {
- if (currentSession != mActiveRestoreSession) {
- Slog.e(TAG, "ending non-current restore session");
- } else {
- if (DEBUG) Slog.v(TAG, "Clearing restore session and halting timeout");
- mActiveRestoreSession = null;
- mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
- }
- }
- }
-
- // Note that a currently-active backup agent has notified us that it has
- // completed the given outstanding asynchronous backup/restore operation.
- @Override
- public void opComplete(int token, long result) {
- if (MORE_DEBUG) {
- Slog.v(TAG, "opComplete: " + Integer.toHexString(token) + " result=" + result);
- }
- Operation op = null;
- synchronized (mCurrentOpLock) {
- op = mCurrentOperations.get(token);
- if (op != null) {
- if (op.state == OP_TIMEOUT) {
- // The operation already timed out, and this is a late response. Tidy up
- // and ignore it; we've already dealt with the timeout.
- op = null;
- mCurrentOperations.delete(token);
- } else if (op.state == OP_ACKNOWLEDGED) {
- if (DEBUG) {
- Slog.w(TAG, "Received duplicate ack for token=" +
- Integer.toHexString(token));
- }
- op = null;
- mCurrentOperations.remove(token);
- } else if (op.state == OP_PENDING) {
- // Can't delete op from mCurrentOperations. waitUntilOperationComplete can be
- // called after we we receive this call.
- op.state = OP_ACKNOWLEDGED;
- }
- }
- mCurrentOpLock.notifyAll();
- }
-
- // The completion callback, if any, is invoked on the handler
- if (op != null && op.callback != null) {
- Pair<BackupRestoreTask, Long> callbackAndResult = Pair.create(op.callback, result);
- Message msg = mBackupHandler.obtainMessage(MSG_OP_COMPLETE, callbackAndResult);
- mBackupHandler.sendMessage(msg);
- }
- }
-
- @Override
- public boolean isAppEligibleForBackup(String packageName) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "isAppEligibleForBackup");
- try {
- PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName,
- PackageManager.GET_SIGNATURES);
- if (!appIsEligibleForBackup(packageInfo.applicationInfo, mPackageManager) ||
- appIsStopped(packageInfo.applicationInfo)) {
- return false;
- }
- IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
- if (transport != null) {
- try {
- return transport.isAppEligibleForBackup(packageInfo,
- appGetsFullBackup(packageInfo));
- } catch (Exception e) {
- Slog.e(TAG, "Unable to ask about eligibility: " + e.getMessage());
- }
- }
- // If transport is not present we couldn't tell that the package is not eligible.
- return true;
- } catch (NameNotFoundException e) {
- return false;
- }
- }
-
- // ----- Restore session -----
-
- class ActiveRestoreSession extends IRestoreSession.Stub {
- private static final String TAG = "RestoreSession";
-
- private String mPackageName;
- private IBackupTransport mRestoreTransport = null;
- RestoreSet[] mRestoreSets = null;
- boolean mEnded = false;
- boolean mTimedOut = false;
-
- ActiveRestoreSession(String packageName, String transport) {
- mPackageName = packageName;
- mRestoreTransport = mTransportManager.getTransportBinder(transport);
- }
-
- public void markTimedOut() {
- mTimedOut = true;
- }
-
- // --- Binder interface ---
- public synchronized int getAvailableRestoreSets(IRestoreObserver observer,
- IBackupManagerMonitor monitor) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "getAvailableRestoreSets");
- if (observer == null) {
- throw new IllegalArgumentException("Observer must not be null");
- }
-
- if (mEnded) {
- throw new IllegalStateException("Restore session already ended");
- }
-
- if (mTimedOut) {
- Slog.i(TAG, "Session already timed out");
- return -1;
- }
-
- long oldId = Binder.clearCallingIdentity();
- try {
- if (mRestoreTransport == null) {
- Slog.w(TAG, "Null transport getting restore sets");
- return -1;
- }
-
- // We know we're doing legit work now, so halt the timeout
- // until we're done. It gets started again when the result
- // comes in.
- mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
-
- // spin off the transport request to our service thread
- mWakelock.acquire();
- Message msg = mBackupHandler.obtainMessage(MSG_RUN_GET_RESTORE_SETS,
- new RestoreGetSetsParams(mRestoreTransport, this, observer,
- monitor));
- mBackupHandler.sendMessage(msg);
- return 0;
- } catch (Exception e) {
- Slog.e(TAG, "Error in getAvailableRestoreSets", e);
- return -1;
- } finally {
- Binder.restoreCallingIdentity(oldId);
- }
- }
-
- public synchronized int restoreAll(long token, IRestoreObserver observer,
- IBackupManagerMonitor monitor) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "performRestore");
-
- if (DEBUG) Slog.d(TAG, "restoreAll token=" + Long.toHexString(token)
- + " observer=" + observer);
-
- if (mEnded) {
- throw new IllegalStateException("Restore session already ended");
- }
-
- if (mTimedOut) {
- Slog.i(TAG, "Session already timed out");
- return -1;
- }
-
- if (mRestoreTransport == null || mRestoreSets == null) {
- Slog.e(TAG, "Ignoring restoreAll() with no restore set");
- return -1;
- }
-
- if (mPackageName != null) {
- Slog.e(TAG, "Ignoring restoreAll() on single-package session");
- return -1;
- }
-
- String dirName;
- try {
- dirName = mRestoreTransport.transportDirName();
- } catch (Exception e) {
- // Transport went AWOL; fail.
- Slog.e(TAG, "Unable to get transport dir for restore: " + e.getMessage());
- return -1;
- }
-
- synchronized (mQueueLock) {
- for (int i = 0; i < mRestoreSets.length; i++) {
- if (token == mRestoreSets[i].token) {
- // Real work, so stop the session timeout until we finalize the restore
- mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
-
- long oldId = Binder.clearCallingIdentity();
- mWakelock.acquire();
- if (MORE_DEBUG) {
- Slog.d(TAG, "restoreAll() kicking off");
- }
- Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE);
- msg.obj = new RestoreParams(mRestoreTransport, dirName,
- observer, monitor, token);
- mBackupHandler.sendMessage(msg);
- Binder.restoreCallingIdentity(oldId);
- return 0;
- }
- }
- }
-
- Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found");
- return -1;
- }
-
- // Restores of more than a single package are treated as 'system' restores
- public synchronized int restoreSome(long token, IRestoreObserver observer,
- IBackupManagerMonitor monitor, String[] packages) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "performRestore");
-
- if (DEBUG) {
- StringBuilder b = new StringBuilder(128);
- b.append("restoreSome token=");
- b.append(Long.toHexString(token));
- b.append(" observer=");
- b.append(observer.toString());
- b.append(" monitor=");
- if (monitor == null) {
- b.append("null");
- } else {
- b.append(monitor.toString());
- }
- b.append(" packages=");
- if (packages == null) {
- b.append("null");
- } else {
- b.append('{');
- boolean first = true;
- for (String s : packages) {
- if (!first) {
- b.append(", ");
- } else first = false;
- b.append(s);
- }
- b.append('}');
- }
- Slog.d(TAG, b.toString());
- }
-
- if (mEnded) {
- throw new IllegalStateException("Restore session already ended");
- }
-
- if (mTimedOut) {
- Slog.i(TAG, "Session already timed out");
- return -1;
- }
-
- if (mRestoreTransport == null || mRestoreSets == null) {
- Slog.e(TAG, "Ignoring restoreAll() with no restore set");
- return -1;
- }
-
- if (mPackageName != null) {
- Slog.e(TAG, "Ignoring restoreAll() on single-package session");
- return -1;
- }
-
- String dirName;
- try {
- dirName = mRestoreTransport.transportDirName();
- } catch (Exception e) {
- // Transport went AWOL; fail.
- Slog.e(TAG, "Unable to get transport name for restoreSome: " + e.getMessage());
- return -1;
- }
-
- synchronized (mQueueLock) {
- for (int i = 0; i < mRestoreSets.length; i++) {
- if (token == mRestoreSets[i].token) {
- // Stop the session timeout until we finalize the restore
- mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
-
- long oldId = Binder.clearCallingIdentity();
- mWakelock.acquire();
- if (MORE_DEBUG) {
- Slog.d(TAG, "restoreSome() of " + packages.length + " packages");
- }
- Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE);
- msg.obj = new RestoreParams(mRestoreTransport, dirName, observer, monitor,
- token, packages, packages.length > 1);
- mBackupHandler.sendMessage(msg);
- Binder.restoreCallingIdentity(oldId);
- return 0;
- }
- }
- }
-
- Slog.w(TAG, "Restore token " + Long.toHexString(token) + " not found");
- return -1;
- }
-
- public synchronized int restorePackage(String packageName, IRestoreObserver observer,
- IBackupManagerMonitor monitor) {
- if (DEBUG) Slog.v(TAG, "restorePackage pkg=" + packageName + " obs=" + observer
- + "monitor=" + monitor);
-
- if (mEnded) {
- throw new IllegalStateException("Restore session already ended");
- }
-
- if (mTimedOut) {
- Slog.i(TAG, "Session already timed out");
- return -1;
- }
-
- if (mPackageName != null) {
- if (! mPackageName.equals(packageName)) {
- Slog.e(TAG, "Ignoring attempt to restore pkg=" + packageName
- + " on session for package " + mPackageName);
- return -1;
- }
- }
-
- PackageInfo app = null;
- try {
- app = mPackageManager.getPackageInfo(packageName, 0);
- } catch (NameNotFoundException nnf) {
- Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName);
- return -1;
- }
-
- // If the caller is not privileged and is not coming from the target
- // app's uid, throw a permission exception back to the caller.
- int perm = mContext.checkPermission(android.Manifest.permission.BACKUP,
- Binder.getCallingPid(), Binder.getCallingUid());
- if ((perm == PackageManager.PERMISSION_DENIED) &&
- (app.applicationInfo.uid != Binder.getCallingUid())) {
- Slog.w(TAG, "restorePackage: bad packageName=" + packageName
- + " or calling uid=" + Binder.getCallingUid());
- throw new SecurityException("No permission to restore other packages");
- }
-
- // So far so good; we're allowed to try to restore this package.
- long oldId = Binder.clearCallingIdentity();
- try {
- // Check whether there is data for it in the current dataset, falling back
- // to the ancestral dataset if not.
- long token = getAvailableRestoreToken(packageName);
- if (DEBUG) Slog.v(TAG, "restorePackage pkg=" + packageName
- + " token=" + Long.toHexString(token));
-
- // If we didn't come up with a place to look -- no ancestral dataset and
- // the app has never been backed up from this device -- there's nothing
- // to do but return failure.
- if (token == 0) {
- if (DEBUG) Slog.w(TAG, "No data available for this package; not restoring");
- return -1;
- }
-
- String dirName;
- try {
- dirName = mRestoreTransport.transportDirName();
- } catch (Exception e) {
- // Transport went AWOL; fail.
- Slog.e(TAG, "Unable to get transport dir for restorePackage: " + e.getMessage());
- return -1;
- }
-
- // Stop the session timeout until we finalize the restore
- mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
-
- // Ready to go: enqueue the restore request and claim success
- mWakelock.acquire();
- if (MORE_DEBUG) {
- Slog.d(TAG, "restorePackage() : " + packageName);
- }
- Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE);
- msg.obj = new RestoreParams(mRestoreTransport, dirName, observer, monitor,
- token, app);
- mBackupHandler.sendMessage(msg);
- } finally {
- Binder.restoreCallingIdentity(oldId);
- }
- return 0;
- }
-
- // Posted to the handler to tear down a restore session in a cleanly synchronized way
- class EndRestoreRunnable implements Runnable {
- BackupManagerService mBackupManager;
- ActiveRestoreSession mSession;
-
- EndRestoreRunnable(BackupManagerService manager, ActiveRestoreSession session) {
- mBackupManager = manager;
- mSession = session;
- }
-
- public void run() {
- // clean up the session's bookkeeping
- synchronized (mSession) {
- mSession.mRestoreTransport = null;
- mSession.mEnded = true;
- }
-
- // clean up the BackupManagerImpl side of the bookkeeping
- // and cancel any pending timeout message
- mBackupManager.clearRestoreSession(mSession);
- }
- }
-
- public synchronized void endRestoreSession() {
- if (DEBUG) Slog.d(TAG, "endRestoreSession");
-
- if (mTimedOut) {
- Slog.i(TAG, "Session already timed out");
- return;
- }
-
- if (mEnded) {
- throw new IllegalStateException("Restore session already ended");
- }
-
- mBackupHandler.post(new EndRestoreRunnable(BackupManagerService.this, this));
- }
- }
-
- @Override
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) return;
-
- long identityToken = Binder.clearCallingIdentity();
- try {
- if (args != null) {
- for (String arg : args) {
- if ("-h".equals(arg)) {
- pw.println("'dumpsys backup' optional arguments:");
- pw.println(" -h : this help text");
- pw.println(" a[gents] : dump information about defined backup agents");
- return;
- } else if ("agents".startsWith(arg)) {
- dumpAgents(pw);
- return;
- }
- }
- }
- dumpInternal(pw);
- } finally {
- Binder.restoreCallingIdentity(identityToken);
- }
- }
-
- private void dumpAgents(PrintWriter pw) {
- List<PackageInfo> agentPackages = allAgentPackages();
- pw.println("Defined backup agents:");
- for (PackageInfo pkg : agentPackages) {
- pw.print(" ");
- pw.print(pkg.packageName); pw.println(':');
- pw.print(" "); pw.println(pkg.applicationInfo.backupAgentName);
- }
- }
-
- private void dumpInternal(PrintWriter pw) {
- synchronized (mQueueLock) {
- pw.println("Backup Manager is " + (mEnabled ? "enabled" : "disabled")
- + " / " + (!mProvisioned ? "not " : "") + "provisioned / "
- + (this.mPendingInits.size() == 0 ? "not " : "") + "pending init");
- pw.println("Auto-restore is " + (mAutoRestore ? "enabled" : "disabled"));
- if (mBackupRunning) pw.println("Backup currently running");
- pw.println("Last backup pass started: " + mLastBackupPass
- + " (now = " + System.currentTimeMillis() + ')');
- pw.println(" next scheduled: " + KeyValueBackupJob.nextScheduled());
-
- pw.println("Transport whitelist:");
- for (ComponentName transport : mTransportManager.getTransportWhitelist()) {
- pw.print(" ");
- pw.println(transport.flattenToShortString());
- }
-
- pw.println("Available transports:");
- final String[] transports = listAllTransports();
- if (transports != null) {
- for (String t : listAllTransports()) {
- pw.println((t.equals(mTransportManager.getCurrentTransportName()) ? " * " : " ") + t);
- try {
- IBackupTransport transport = mTransportManager.getTransportBinder(t);
- File dir = new File(mBaseStateDir, transport.transportDirName());
- pw.println(" destination: " + transport.currentDestinationString());
- pw.println(" intent: " + transport.configurationIntent());
- for (File f : dir.listFiles()) {
- pw.println(" " + f.getName() + " - " + f.length() + " state bytes");
- }
- } catch (Exception e) {
- Slog.e(TAG, "Error in transport", e);
- pw.println(" Error: " + e);
- }
- }
- }
-
- pw.println("Pending init: " + mPendingInits.size());
- for (String s : mPendingInits) {
- pw.println(" " + s);
- }
-
- if (DEBUG_BACKUP_TRACE) {
- synchronized (mBackupTrace) {
- if (!mBackupTrace.isEmpty()) {
- pw.println("Most recent backup trace:");
- for (String s : mBackupTrace) {
- pw.println(" " + s);
- }
- }
- }
- }
-
- pw.print("Ancestral: "); pw.println(Long.toHexString(mAncestralToken));
- pw.print("Current: "); pw.println(Long.toHexString(mCurrentToken));
-
- int N = mBackupParticipants.size();
- pw.println("Participants:");
- for (int i=0; i<N; i++) {
- int uid = mBackupParticipants.keyAt(i);
- pw.print(" uid: ");
- pw.println(uid);
- HashSet<String> participants = mBackupParticipants.valueAt(i);
- for (String app: participants) {
- pw.println(" " + app);
- }
- }
-
- pw.println("Ancestral packages: "
- + (mAncestralPackages == null ? "none" : mAncestralPackages.size()));
- if (mAncestralPackages != null) {
- for (String pkg : mAncestralPackages) {
- pw.println(" " + pkg);
- }
- }
-
- pw.println("Ever backed up: " + mEverStoredApps.size());
- for (String pkg : mEverStoredApps) {
- pw.println(" " + pkg);
- }
-
- pw.println("Pending key/value backup: " + mPendingBackups.size());
- for (BackupRequest req : mPendingBackups.values()) {
- pw.println(" " + req);
- }
-
- pw.println("Full backup queue:" + mFullBackupQueue.size());
- for (FullBackupEntry entry : mFullBackupQueue) {
- pw.print(" "); pw.print(entry.lastBackup);
- pw.print(" : "); pw.println(entry.packageName);
- }
- }
- }
-
- private static void sendBackupOnUpdate(IBackupObserver observer, String packageName,
- BackupProgress progress) {
- if (observer != null) {
- try {
- observer.onUpdate(packageName, progress);
- } catch (RemoteException e) {
- if (DEBUG) {
- Slog.w(TAG, "Backup observer went away: onUpdate");
- }
- }
- }
- }
-
- private static void sendBackupOnPackageResult(IBackupObserver observer, String packageName,
- int status) {
- if (observer != null) {
- try {
- observer.onResult(packageName, status);
- } catch (RemoteException e) {
- if (DEBUG) {
- Slog.w(TAG, "Backup observer went away: onResult");
- }
- }
- }
- }
-
- private static void sendBackupFinished(IBackupObserver observer, int status) {
- if (observer != null) {
- try {
- observer.backupFinished(status);
- } catch (RemoteException e) {
- if (DEBUG) {
- Slog.w(TAG, "Backup observer went away: backupFinished");
- }
- }
- }
- }
-
- private Bundle putMonitoringExtra(Bundle extras, String key, String value) {
- if (extras == null) {
- extras = new Bundle();
- }
- extras.putString(key, value);
- return extras;
- }
-
- private Bundle putMonitoringExtra(Bundle extras, String key, int value) {
- if (extras == null) {
- extras = new Bundle();
- }
- extras.putInt(key, value);
- return extras;
- }
-
- private Bundle putMonitoringExtra(Bundle extras, String key, long value) {
- if (extras == null) {
- extras = new Bundle();
- }
- extras.putLong(key, value);
- return extras;
- }
-
-
- private Bundle putMonitoringExtra(Bundle extras, String key, boolean value) {
- if (extras == null) {
- extras = new Bundle();
- }
- extras.putBoolean(key, value);
- return extras;
- }
-
- private static IBackupManagerMonitor monitorEvent(IBackupManagerMonitor monitor, int id,
- PackageInfo pkg, int category, Bundle extras) {
- if (monitor != null) {
- try {
- Bundle bundle = new Bundle();
- bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id);
- bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_CATEGORY, category);
- if (pkg != null) {
- bundle.putString(EXTRA_LOG_EVENT_PACKAGE_NAME,
- pkg.packageName);
- bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION,
- pkg.versionCode);
- }
- if (extras != null) {
- bundle.putAll(extras);
- }
- monitor.onEvent(bundle);
- return monitor;
- } catch(RemoteException e) {
- if (DEBUG) {
- Slog.w(TAG, "backup manager monitor went away");
- }
- }
- }
- return null;
- }
-
- @Override
- public IBackupManager getBackupManagerBinder() {
- return mBackupManagerBinder;
- }
-
-}
diff --git a/com/android/server/backup/BackupManagerServiceInterface.java b/com/android/server/backup/BackupManagerServiceInterface.java
index 041f9ed5..86462d85 100644
--- a/com/android/server/backup/BackupManagerServiceInterface.java
+++ b/com/android/server/backup/BackupManagerServiceInterface.java
@@ -120,6 +120,15 @@ public interface BackupManagerServiceInterface {
// Report whether the backup mechanism is currently enabled
boolean isBackupEnabled();
+ // Update the transport attributes
+ void updateTransportAttributes(
+ ComponentName transportComponent,
+ String name,
+ Intent configurationIntent,
+ String currentDestinationString,
+ Intent dataManagementIntent,
+ String dataManagementLabel);
+
// Report the name of the currently active transport
String getCurrentTransport();
diff --git a/com/android/server/backup/FileMetadata.java b/com/android/server/backup/FileMetadata.java
index 5465609f..3d260cba 100644
--- a/com/android/server/backup/FileMetadata.java
+++ b/com/android/server/backup/FileMetadata.java
@@ -36,7 +36,7 @@ public class FileMetadata {
public long mode; // e.g. 0666 (actually int)
public long mtime; // last mod time, UTC time_t (actually int)
public long size; // bytes of content
- public int version; // App version.
+ public long version; // App version.
public boolean hasApk; // Whether backup file contains apk.
@Override
diff --git a/com/android/server/backup/PackageManagerBackupAgent.java b/com/android/server/backup/PackageManagerBackupAgent.java
index f658f22d..2d2993dc 100644
--- a/com/android/server/backup/PackageManagerBackupAgent.java
+++ b/com/android/server/backup/PackageManagerBackupAgent.java
@@ -99,10 +99,10 @@ public class PackageManagerBackupAgent extends BackupAgent {
// For compactness we store the SHA-256 hash of each app's Signatures
// rather than the Signature blocks themselves.
public class Metadata {
- public int versionCode;
+ public long versionCode;
public ArrayList<byte[]> sigHashes;
- Metadata(int version, ArrayList<byte[]> hashes) {
+ Metadata(long version, ArrayList<byte[]> hashes) {
versionCode = version;
sigHashes = hashes;
}
@@ -206,7 +206,7 @@ public class PackageManagerBackupAgent extends BackupAgent {
homeInfo = mPackageManager.getPackageInfo(home.getPackageName(),
PackageManager.GET_SIGNATURES);
homeInstaller = mPackageManager.getInstallerPackageName(home.getPackageName());
- homeVersion = homeInfo.versionCode;
+ homeVersion = homeInfo.getLongVersionCode();
homeSigHashes = BackupUtils.hashSignatureArray(homeInfo.signatures);
} catch (NameNotFoundException e) {
Slog.w(TAG, "Can't access preferred home info");
@@ -287,7 +287,7 @@ public class PackageManagerBackupAgent extends BackupAgent {
// metadata again. In either case, take it out of mExisting so that
// we don't consider it deleted later.
mExisting.remove(packName);
- if (info.versionCode == mStateVersions.get(packName).versionCode) {
+ if (info.getLongVersionCode() == mStateVersions.get(packName).versionCode) {
continue;
}
}
@@ -309,13 +309,18 @@ public class PackageManagerBackupAgent extends BackupAgent {
// marshal the version code in a canonical form
outputBuffer.reset();
- outputBufferStream.writeInt(info.versionCode);
+ if (info.versionCodeMajor != 0) {
+ outputBufferStream.writeInt(Integer.MIN_VALUE);
+ outputBufferStream.writeLong(info.getLongVersionCode());
+ } else {
+ outputBufferStream.writeInt(info.versionCode);
+ }
writeSignatureHashArray(outputBufferStream,
BackupUtils.hashSignatureArray(info.signatures));
if (DEBUG) {
Slog.v(TAG, "+ writing metadata for " + packName
- + " version=" + info.versionCode
+ + " version=" + info.getLongVersionCode()
+ " entityLen=" + outputBuffer.size());
}
@@ -409,7 +414,13 @@ public class PackageManagerBackupAgent extends BackupAgent {
}
} else {
// it's a file metadata record
- int versionCode = inputBufferStream.readInt();
+ int versionCodeInt = inputBufferStream.readInt();
+ long versionCode;
+ if (versionCodeInt == Integer.MIN_VALUE) {
+ versionCode = inputBufferStream.readLong();
+ } else {
+ versionCode = versionCodeInt;
+ }
ArrayList<byte[]> sigs = readSignatureHashArray(inputBufferStream);
if (DEBUG) {
Slog.i(TAG, " read metadata for " + key
@@ -561,7 +572,13 @@ public class PackageManagerBackupAgent extends BackupAgent {
// The global metadata was last; now read all the apps
while (true) {
pkg = in.readUTF();
- int versionCode = in.readInt();
+ int versionCodeInt = in.readInt();
+ long versionCode;
+ if (versionCodeInt == Integer.MIN_VALUE) {
+ versionCode = in.readLong();
+ } else {
+ versionCode = versionCodeInt;
+ }
if (!ignoreExisting) {
mExisting.add(pkg);
@@ -609,7 +626,12 @@ public class PackageManagerBackupAgent extends BackupAgent {
// now write all the app names + versions
for (PackageInfo pkg : pkgs) {
out.writeUTF(pkg.packageName);
- out.writeInt(pkg.versionCode);
+ if (pkg.versionCodeMajor != 0) {
+ out.writeInt(Integer.MIN_VALUE);
+ out.writeLong(pkg.getLongVersionCode());
+ } else {
+ out.writeInt(pkg.versionCode);
+ }
}
out.flush();
diff --git a/com/android/server/backup/RefactoredBackupManagerService.java b/com/android/server/backup/RefactoredBackupManagerService.java
index 2788218d..3a374598 100644
--- a/com/android/server/backup/RefactoredBackupManagerService.java
+++ b/com/android/server/backup/RefactoredBackupManagerService.java
@@ -25,7 +25,6 @@ import static com.android.server.backup.internal.BackupHandler.MSG_REQUEST_BACKU
import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT;
import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT;
import static com.android.server.backup.internal.BackupHandler.MSG_RETRY_CLEAR;
-import static com.android.server.backup.internal.BackupHandler.MSG_RETRY_INIT;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_BACKUP;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_ADB_RESTORE;
import static com.android.server.backup.internal.BackupHandler.MSG_RUN_CLEAR;
@@ -91,8 +90,10 @@ import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.backup.IBackupTransport;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.Preconditions;
import com.android.server.AppWidgetBackupBridge;
import com.android.server.EventLogTags;
import com.android.server.SystemConfig;
@@ -118,6 +119,7 @@ import com.android.server.backup.params.RestoreParams;
import com.android.server.backup.restore.ActiveRestoreSession;
import com.android.server.backup.restore.PerformUnifiedRestoreTask;
import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportNotRegisteredException;
import com.android.server.backup.utils.AppBackupUtils;
import com.android.server.backup.utils.BackupManagerMonitorUtils;
import com.android.server.backup.utils.BackupObserverUtils;
@@ -201,6 +203,8 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
public static final String RUN_BACKUP_ACTION = "android.app.backup.intent.RUN";
public static final String RUN_INITIALIZE_ACTION = "android.app.backup.intent.INIT";
+ public static final String BACKUP_FINISHED_ACTION = "android.intent.action.BACKUP_FINISHED";
+ public static final String BACKUP_FINISHED_PACKAGE_EXTRA = "packageName";
// Timeout interval for deciding that a bind or clear-data has taken too long
private static final long TIMEOUT_INTERVAL = 10 * 1000;
@@ -723,8 +727,54 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
// ----- Main service implementation -----
- public RefactoredBackupManagerService(Context context, Trampoline parent,
+ public static RefactoredBackupManagerService create(
+ Context context,
+ Trampoline parent,
HandlerThread backupThread) {
+ // Set up our transport options and initialize the default transport
+ SystemConfig systemConfig = SystemConfig.getInstance();
+ Set<ComponentName> transportWhitelist = systemConfig.getBackupTransportWhitelist();
+
+ String transport =
+ Settings.Secure.getString(
+ context.getContentResolver(), Settings.Secure.BACKUP_TRANSPORT);
+ if (TextUtils.isEmpty(transport)) {
+ transport = null;
+ }
+ if (DEBUG) {
+ Slog.v(TAG, "Starting with transport " + transport);
+ }
+ TransportManager transportManager =
+ new TransportManager(
+ context,
+ transportWhitelist,
+ transport,
+ backupThread.getLooper());
+
+ // If encrypted file systems is enabled or disabled, this call will return the
+ // correct directory.
+ File baseStateDir = new File(Environment.getDataDirectory(), "backup");
+
+ // This dir on /cache is managed directly in init.rc
+ File dataDir = new File(Environment.getDownloadCacheDirectory(), "backup_stage");
+
+ return new RefactoredBackupManagerService(
+ context,
+ parent,
+ backupThread,
+ baseStateDir,
+ dataDir,
+ transportManager);
+ }
+
+ @VisibleForTesting
+ RefactoredBackupManagerService(
+ Context context,
+ Trampoline parent,
+ HandlerThread backupThread,
+ File baseStateDir,
+ File dataDir,
+ TransportManager transportManager) {
mContext = context;
mPackageManager = context.getPackageManager();
mPackageManagerBinder = AppGlobals.getPackageManager();
@@ -751,16 +801,13 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
false, mProvisionedObserver);
- // If Encrypted file systems is enabled or disabled, this call will return the
- // correct directory.
- mBaseStateDir = new File(Environment.getDataDirectory(), "backup");
+ mBaseStateDir = baseStateDir;
mBaseStateDir.mkdirs();
if (!SELinux.restorecon(mBaseStateDir)) {
Slog.e(TAG, "SELinux restorecon failed on " + mBaseStateDir);
}
- // This dir on /cache is managed directly in init.rc
- mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup_stage");
+ mDataDir = dataDir;
mBackupPasswordManager = new BackupPasswordManager(mContext, mBaseStateDir, mRng);
@@ -803,26 +850,13 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
addPackageParticipantsLocked(null);
}
- // Set up our transport options and initialize the default transport
- // TODO: Don't create transports that we don't need to?
- SystemConfig systemConfig = SystemConfig.getInstance();
- Set<ComponentName> transportWhitelist = systemConfig.getBackupTransportWhitelist();
-
- String transport = Settings.Secure.getString(context.getContentResolver(),
- Settings.Secure.BACKUP_TRANSPORT);
- if (TextUtils.isEmpty(transport)) {
- transport = null;
- }
- String currentTransport = transport;
- if (DEBUG) Slog.v(TAG, "Starting with transport " + currentTransport);
-
- mTransportManager = new TransportManager(context, transportWhitelist, currentTransport,
- mTransportBoundListener, backupThread.getLooper());
+ mTransportManager = transportManager;
+ mTransportManager.setTransportBoundListener(mTransportBoundListener);
mTransportManager.registerAllTransports();
// Now that we know about valid backup participants, parse any
// leftover journal files into the pending backup set
- mBackupHandler.post(() -> parseLeftoverJournals());
+ mBackupHandler.post(this::parseLeftoverJournals);
// Power management
mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*");
@@ -1051,56 +1085,35 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
return mBackupPasswordManager.backupPasswordMatches(currentPw);
}
- // Maintain persistent state around whether need to do an initialize operation.
- // Must be called with the queue lock held.
- public void recordInitPendingLocked(boolean isPending, String transportName) {
- if (MORE_DEBUG) {
- Slog.i(TAG, "recordInitPendingLocked: " + isPending
- + " on transport " + transportName);
- }
- mBackupHandler.removeMessages(MSG_RETRY_INIT);
+ /**
+ * Maintain persistent state around whether need to do an initialize operation. This will lock
+ * on {@link #getQueueLock()}.
+ */
+ public void recordInitPending(
+ boolean isPending, String transportName, String transportDirName) {
+ synchronized (mQueueLock) {
+ if (MORE_DEBUG) {
+ Slog.i(TAG, "recordInitPending(" + isPending + ") on transport " + transportName);
+ }
- try {
- IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
- if (transport != null) {
- String transportDirName = transport.transportDirName();
- File stateDir = new File(mBaseStateDir, transportDirName);
- File initPendingFile = new File(stateDir, INIT_SENTINEL_FILE_NAME);
-
- if (isPending) {
- // We need an init before we can proceed with sending backup data.
- // Record that with an entry in our set of pending inits, as well as
- // journaling it via creation of a sentinel file.
- mPendingInits.add(transportName);
- try {
- (new FileOutputStream(initPendingFile)).close();
- } catch (IOException ioe) {
- // Something is badly wrong with our permissions; just try to move on
- }
- } else {
- // No more initialization needed; wipe the journal and reset our state.
- initPendingFile.delete();
- mPendingInits.remove(transportName);
+ File stateDir = new File(mBaseStateDir, transportDirName);
+ File initPendingFile = new File(stateDir, INIT_SENTINEL_FILE_NAME);
+
+ if (isPending) {
+ // We need an init before we can proceed with sending backup data.
+ // Record that with an entry in our set of pending inits, as well as
+ // journaling it via creation of a sentinel file.
+ mPendingInits.add(transportName);
+ try {
+ (new FileOutputStream(initPendingFile)).close();
+ } catch (IOException ioe) {
+ // Something is badly wrong with our permissions; just try to move on
}
- return; // done; don't fall through to the error case
+ } else {
+ // No more initialization needed; wipe the journal and reset our state.
+ initPendingFile.delete();
+ mPendingInits.remove(transportName);
}
- } catch (Exception e) {
- // transport threw when asked its name; fall through to the lookup-failed case
- Slog.e(TAG, "Transport " + transportName + " failed to report name: "
- + e.getMessage());
- }
-
- // The named transport doesn't exist or threw. This operation is
- // important, so we record the need for a an init and post a message
- // to retry the init later.
- if (isPending) {
- mPendingInits.add(transportName);
- mBackupHandler.sendMessageDelayed(
- mBackupHandler.obtainMessage(MSG_RETRY_INIT,
- (isPending ? 1 : 0),
- 0,
- transportName),
- TRANSPORT_RETRY_INTERVAL);
}
}
@@ -1407,6 +1420,14 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
public void logBackupComplete(String packageName) {
if (packageName.equals(PACKAGE_MANAGER_SENTINEL)) return;
+ for (String receiver : mConstants.getBackupFinishedNotificationReceivers()) {
+ final Intent notification = new Intent();
+ notification.setAction(BACKUP_FINISHED_ACTION);
+ notification.setPackage(receiver);
+ notification.putExtra(BACKUP_FINISHED_PACKAGE_EXTRA, packageName);
+ mContext.sendBroadcastAsUser(notification, UserHandle.OWNER);
+ }
+
mProcessedPackagesJournal.addPackage(packageName);
}
@@ -1438,14 +1459,6 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
}
}
- // What name is this transport registered under...?
- private String getTransportName(IBackupTransport transport) {
- if (MORE_DEBUG) {
- Slog.v(TAG, "Searching for transport name of " + transport);
- }
- return mTransportManager.getTransportName(transport);
- }
-
// fire off a backup agent, blocking until it attaches or times out
@Override
public IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode) {
@@ -1496,7 +1509,11 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
}
// clear an application's data, blocking until the operation completes or times out
- public void clearApplicationDataSynchronous(String packageName) {
+ // if keepSystemState is true, we intentionally do not also clear system state that
+ // would ordinarily also be cleared, because we aren't actually wiping the app back
+ // to empty; we're bringing it into the actual expected state related to the already-
+ // restored notification state etc.
+ public void clearApplicationDataSynchronous(String packageName, boolean keepSystemState) {
// Don't wipe packages marked allowClearUserData=false
try {
PackageInfo info = mPackageManager.getPackageInfo(packageName, 0);
@@ -1517,7 +1534,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
synchronized (mClearDataLock) {
mClearingData = true;
try {
- mActivityManager.clearApplicationUserData(packageName, observer, 0);
+ mActivityManager.clearApplicationUserData(packageName, keepSystemState, observer, 0);
} catch (RemoteException e) {
// can't happen because the activity manager is in this process
}
@@ -1586,27 +1603,9 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
return BackupManager.ERROR_BACKUP_NOT_ALLOWED;
}
- // We're using pieces of the new binding on-demand infra-structure and the old always-bound
- // infra-structure below this comment. The TransportManager.getCurrentTransportClient() line
- // is using the new one and TransportManager.getCurrentTransportBinder() is using the old.
- // This is weird but there is a reason.
- // This is the natural place to put TransportManager.getCurrentTransportClient() because of
- // the null handling below that should be the same for TransportClient.
- // TransportClient.connect() would return a IBackupTransport for us (instead of using the
- // old infra), but it may block and we don't want this in this thread.
- // The only usage of transport in this method is for transport.transportDirName(). When the
- // push-from-transport part of binding on-demand is in place we will replace the calls for
- // IBackupTransport.transportDirName() with calls for
- // TransportManager.transportDirName(transportName) or similar. So we'll leave the old piece
- // here until we implement that.
- // TODO(brufino): Remove always-bound code mTransportManager.getCurrentTransportBinder()
TransportClient transportClient =
mTransportManager.getCurrentTransportClient("BMS.requestBackup()");
- IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
- if (transportClient == null || transport == null) {
- if (transportClient != null) {
- mTransportManager.disposeOfTransportClient(transportClient, "BMS.requestBackup()");
- }
+ if (transportClient == null) {
BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
monitor = BackupManagerMonitorUtils.monitorEvent(monitor,
BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL,
@@ -1651,15 +1650,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
+ " k/v backups");
}
- String dirName;
- try {
- dirName = transport.transportDirName();
- } catch (Exception e) {
- Slog.e(TAG, "Transport unavailable while attempting backup: " + e.getMessage());
- BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
- return BackupManager.ERROR_TRANSPORT_ABORTED;
- }
-
+ String dirName = transportClient.getTransportDirName();
boolean nonIncrementalBackup = (flags & BackupManager.FLAG_NON_INCREMENTAL_BACKUP) != 0;
Message msg = mBackupHandler.obtainMessage(MSG_REQUEST_BACKUP);
@@ -1970,16 +1961,17 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
writeFullBackupScheduleAsync();
}
- private boolean fullBackupAllowable(IBackupTransport transport) {
- if (transport == null) {
- Slog.w(TAG, "Transport not present; full data backup not performed");
+ private boolean fullBackupAllowable(String transportName) {
+ if (!mTransportManager.isTransportRegistered(transportName)) {
+ Slog.w(TAG, "Transport not registered; full data backup not performed");
return false;
}
// Don't proceed unless we have already established package metadata
// for the current dataset via a key/value backup pass.
try {
- File stateDir = new File(mBaseStateDir, transport.transportDirName());
+ String transportDirName = mTransportManager.getTransportDirName(transportName);
+ File stateDir = new File(mBaseStateDir, transportDirName);
File pmState = new File(stateDir, PACKAGE_MANAGER_SENTINEL);
if (pmState.length() <= 0) {
if (DEBUG) {
@@ -2069,7 +2061,8 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
headBusy = false;
- if (!fullBackupAllowable(mTransportManager.getCurrentTransportBinder())) {
+ String transportName = mTransportManager.getCurrentTransportName();
+ if (!fullBackupAllowable(transportName)) {
if (MORE_DEBUG) {
Slog.i(TAG, "Preconditions not met; not running full backup");
}
@@ -2328,7 +2321,9 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
final long oldId = Binder.clearCallingIdentity();
try {
mWakelock.acquire();
- mBackupHandler.post(new PerformInitializeTask(this, transportNames, observer));
+ OnTaskFinishedListener listener = caller -> mWakelock.release();
+ mBackupHandler.post(
+ new PerformInitializeTask(this, transportNames, observer, listener));
} finally {
Binder.restoreCallingIdentity(oldId);
}
@@ -2365,19 +2360,24 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
if (MORE_DEBUG) Slog.v(TAG, "Found the app - running clear process");
mBackupHandler.removeMessages(MSG_RETRY_CLEAR);
synchronized (mQueueLock) {
- final IBackupTransport transport =
- mTransportManager.getTransportBinder(transportName);
- if (transport == null) {
- // transport is currently unavailable -- make sure to retry
+ TransportClient transportClient =
+ mTransportManager
+ .getTransportClient(transportName, "BMS.clearBackupData()");
+ if (transportClient == null) {
+ // transport is currently unregistered -- make sure to retry
Message msg = mBackupHandler.obtainMessage(MSG_RETRY_CLEAR,
new ClearRetryParams(transportName, packageName));
mBackupHandler.sendMessageDelayed(msg, TRANSPORT_RETRY_INTERVAL);
return;
}
long oldId = Binder.clearCallingIdentity();
+ OnTaskFinishedListener listener =
+ caller ->
+ mTransportManager.disposeOfTransportClient(transportClient, caller);
mWakelock.acquire();
- Message msg = mBackupHandler.obtainMessage(MSG_RUN_CLEAR,
- new ClearParams(transport, info));
+ Message msg = mBackupHandler.obtainMessage(
+ MSG_RUN_CLEAR,
+ new ClearParams(transportClient, info, listener));
mBackupHandler.sendMessage(msg);
Binder.restoreCallingIdentity(oldId);
}
@@ -2512,7 +2512,8 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
throw new IllegalStateException("Restore supported only for the device owner");
}
- if (!fullBackupAllowable(mTransportManager.getCurrentTransportBinder())) {
+ String transportName = mTransportManager.getCurrentTransportName();
+ if (!fullBackupAllowable(transportName)) {
Slog.i(TAG, "Full backup not currently possible -- key/value backup not yet run?");
} else {
if (DEBUG) {
@@ -2793,10 +2794,30 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
if (wasEnabled && mProvisioned) {
// NOTE: we currently flush every registered transport, not just
// the currently-active one.
- String[] allTransports = mTransportManager.getBoundTransportNames();
+ List<String> transportNames = new ArrayList<>();
+ List<String> transportDirNames = new ArrayList<>();
+ mTransportManager.forEachRegisteredTransport(
+ name -> {
+ final String dirName;
+ try {
+ dirName =
+ mTransportManager
+ .getTransportDirName(name);
+ } catch (TransportNotRegisteredException e) {
+ // Should never happen
+ Slog.e(TAG, "Unexpected unregistered transport", e);
+ return;
+ }
+ transportNames.add(name);
+ transportDirNames.add(dirName);
+ });
+
// build the set of transports for which we are posting an init
- for (String transport : allTransports) {
- recordInitPendingLocked(true, transport);
+ for (int i = 0; i < transportNames.size(); i++) {
+ recordInitPending(
+ true,
+ transportNames.get(i),
+ transportDirNames.get(i));
}
mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
mRunInitIntent);
@@ -2885,6 +2906,93 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
return whitelistedTransports;
}
+ /**
+ * 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.
+ */
+ @Override
+ public void updateTransportAttributes(
+ ComponentName transportComponent,
+ String name,
+ @Nullable Intent configurationIntent,
+ String currentDestinationString,
+ @Nullable Intent dataManagementIntent,
+ @Nullable String dataManagementLabel) {
+ updateTransportAttributes(
+ Binder.getCallingUid(),
+ transportComponent,
+ name,
+ configurationIntent,
+ currentDestinationString,
+ dataManagementIntent,
+ dataManagementLabel);
+ }
+
+ @VisibleForTesting
+ void updateTransportAttributes(
+ int callingUid,
+ ComponentName transportComponent,
+ String name,
+ @Nullable Intent configurationIntent,
+ String currentDestinationString,
+ @Nullable Intent dataManagementIntent,
+ @Nullable String dataManagementLabel) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.BACKUP, "updateTransportAttributes");
+
+ Preconditions.checkNotNull(transportComponent, "transportComponent can't be null");
+ Preconditions.checkNotNull(name, "name can't be null");
+ Preconditions.checkNotNull(
+ currentDestinationString, "currentDestinationString can't be null");
+ Preconditions.checkArgument(
+ (dataManagementIntent == null) == (dataManagementLabel == null),
+ "dataManagementLabel should be null iff dataManagementIntent is null");
+
+ try {
+ int transportUid =
+ mContext.getPackageManager()
+ .getPackageUid(transportComponent.getPackageName(), 0);
+ if (callingUid != transportUid) {
+ throw new SecurityException("Only the transport can change its description");
+ }
+ } catch (NameNotFoundException e) {
+ throw new SecurityException("Transport package not found", e);
+ }
+
+ final long oldId = Binder.clearCallingIdentity();
+ try {
+ mTransportManager.updateTransportAttributes(
+ transportComponent,
+ name,
+ configurationIntent,
+ currentDestinationString,
+ dataManagementIntent,
+ dataManagementLabel);
+ } finally {
+ Binder.restoreCallingIdentity(oldId);
+ }
+ }
+
// Select which transport to use for the next backup operation.
@Override
public String selectBackupTransport(String transport) {
@@ -2973,23 +3081,16 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
public Intent getConfigurationIntent(String transportName) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
"getConfigurationIntent");
-
- final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
- if (transport != null) {
- try {
- final Intent intent = transport.configurationIntent();
- if (MORE_DEBUG) {
- Slog.d(TAG, "getConfigurationIntent() returning config intent "
- + intent);
- }
- return intent;
- } catch (Exception e) {
- /* fall through to return null */
- Slog.e(TAG, "Unable to get configuration intent from transport: " + e.getMessage());
+ try {
+ Intent intent = mTransportManager.getTransportConfigurationIntent(transportName);
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "getConfigurationIntent() returning intent " + intent);
}
+ return intent;
+ } catch (TransportNotRegisteredException e) {
+ Slog.e(TAG, "Unable to get configuration intent from transport: " + e.getMessage());
+ return null;
}
-
- return null;
}
// Supply the configuration summary string for the given transport. If the name is
@@ -3023,22 +3124,16 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
"getDataManagementIntent");
- final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
- if (transport != null) {
- try {
- final Intent intent = transport.dataManagementIntent();
- if (MORE_DEBUG) {
- Slog.d(TAG, "getDataManagementIntent() returning intent "
- + intent);
- }
- return intent;
- } catch (Exception e) {
- /* fall through to return null */
- Slog.e(TAG, "Unable to get management intent from transport: " + e.getMessage());
+ try {
+ Intent intent = mTransportManager.getTransportDataManagementIntent(transportName);
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "getDataManagementIntent() returning intent " + intent);
}
+ return intent;
+ } catch (TransportNotRegisteredException e) {
+ Slog.e(TAG, "Unable to get management intent from transport: " + e.getMessage());
+ return null;
}
-
- return null;
}
// Supply the menu label for affordances that fire the manage-data intent
@@ -3048,19 +3143,16 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
"getDataManagementLabel");
- final IBackupTransport transport = mTransportManager.getTransportBinder(transportName);
- if (transport != null) {
- try {
- final String text = transport.dataManagementLabel();
- if (MORE_DEBUG) Slog.d(TAG, "getDataManagementLabel() returning " + text);
- return text;
- } catch (Exception e) {
- /* fall through to return null */
- Slog.e(TAG, "Unable to get management label from transport: " + e.getMessage());
+ try {
+ String label = mTransportManager.getTransportDataManagementLabel(transportName);
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "getDataManagementLabel() returning " + label);
}
+ return label;
+ } catch (TransportNotRegisteredException e) {
+ Slog.e(TAG, "Unable to get management label from transport: " + e.getMessage());
+ return null;
}
-
- return null;
}
// Callback: a requested backup agent has been instantiated. This should only
@@ -3122,10 +3214,10 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
skip = true;
}
- // Do we have a transport to fetch data for us?
- IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
- if (transport == null) {
- if (DEBUG) Slog.w(TAG, "No transport");
+ TransportClient transportClient =
+ mTransportManager.getCurrentTransportClient("BMS.restoreAtInstall()");
+ if (transportClient == null) {
+ if (DEBUG) Slog.w(TAG, "No transport client");
skip = true;
}
@@ -3142,16 +3234,26 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
// The eventual message back into the Package Manager to run the post-install
// steps for 'token' will be issued from the restore handling code.
- // This can throw and so *must* happen before the wakelock is acquired
- String dirName = transport.transportDirName();
-
mWakelock.acquire();
+
+ OnTaskFinishedListener listener = caller -> {
+ mTransportManager.disposeOfTransportClient(transportClient, caller);
+ mWakelock.release();
+ };
+
if (MORE_DEBUG) {
Slog.d(TAG, "Restore at install of " + packageName);
}
Message msg = mBackupHandler.obtainMessage(MSG_RUN_RESTORE);
- msg.obj = new RestoreParams(transport, dirName, null, null,
- restoreSet, packageName, token);
+ msg.obj =
+ RestoreParams.createForRestoreAtInstall(
+ transportClient,
+ /* observer */ null,
+ /* monitor */ null,
+ restoreSet,
+ packageName,
+ token,
+ listener);
mBackupHandler.sendMessage(msg);
} catch (Exception e) {
// Calling into the transport broke; back off and proceed with the installation.
@@ -3161,8 +3263,14 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
}
if (skip) {
- // Auto-restore disabled or no way to attempt a restore; just tell the Package
- // Manager to proceed with the post-install handling for this package.
+ // Auto-restore disabled or no way to attempt a restore
+
+ if (transportClient != null) {
+ mTransportManager.disposeOfTransportClient(
+ transportClient, "BMS.restoreAtInstall()");
+ }
+
+ // Tell the PackageManager to proceed with the post-install handling for this package.
if (DEBUG) Slog.v(TAG, "Finishing install immediately");
try {
mPackageManagerBinder.finishPackageInstall(token, false);
@@ -3361,14 +3469,16 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
pw.println("Available transports:");
final String[] transports = listAllTransports();
if (transports != null) {
- for (String t : listAllTransports()) {
+ for (String t : transports) {
pw.println((t.equals(mTransportManager.getCurrentTransportName()) ? " * "
: " ") + t);
try {
IBackupTransport transport = mTransportManager.getTransportBinder(t);
- File dir = new File(mBaseStateDir, transport.transportDirName());
+ File dir = new File(mBaseStateDir,
+ mTransportManager.getTransportDirName(t));
pw.println(" destination: " + transport.currentDestinationString());
- pw.println(" intent: " + transport.configurationIntent());
+ pw.println(" intent: "
+ + mTransportManager.getTransportConfigurationIntent(t));
for (File f : dir.listFiles()) {
pw.println(
" " + f.getName() + " - " + f.length() + " state bytes");
diff --git a/com/android/server/backup/Trampoline.java b/com/android/server/backup/Trampoline.java
index 9847edf8..94a26275 100644
--- a/com/android/server/backup/Trampoline.java
+++ b/com/android/server/backup/Trampoline.java
@@ -16,6 +16,7 @@
package com.android.server.backup;
+import android.annotation.Nullable;
import android.app.backup.BackupManager;
import android.app.backup.IBackupManager;
import android.app.backup.IBackupObserver;
@@ -31,14 +32,12 @@ import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
-import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
-import android.provider.Settings;
import android.util.Slog;
import com.android.internal.util.DumpUtils;
@@ -88,25 +87,10 @@ public class Trampoline extends IBackupManager.Stub {
mSuppressFile.getParentFile().mkdirs();
}
- protected BackupManagerServiceInterface createService() {
- if (isRefactoredServiceEnabled()) {
- Slog.i(TAG, "Instantiating RefactoredBackupManagerService");
- return createRefactoredBackupManagerService();
- }
-
- Slog.i(TAG, "Instantiating BackupManagerService");
- return createBackupManagerService();
- }
-
protected boolean isBackupDisabled() {
return SystemProperties.getBoolean(BACKUP_DISABLE_PROPERTY, false);
}
- protected boolean isRefactoredServiceEnabled() {
- return Settings.Global.getInt(mContext.getContentResolver(),
- Settings.Global.BACKUP_REFACTORED_SERVICE_DISABLED, 0) == 0;
- }
-
protected int binderGetCallingUid() {
return Binder.getCallingUid();
}
@@ -117,11 +101,7 @@ public class Trampoline extends IBackupManager.Stub {
}
protected BackupManagerServiceInterface createRefactoredBackupManagerService() {
- return new RefactoredBackupManagerService(mContext, this, mHandlerThread);
- }
-
- protected BackupManagerServiceInterface createBackupManagerService() {
- return new BackupManagerService(mContext, this, mHandlerThread);
+ return RefactoredBackupManagerService.create(mContext, this, mHandlerThread);
}
// internal control API
@@ -137,7 +117,7 @@ public class Trampoline extends IBackupManager.Stub {
synchronized (this) {
if (!mSuppressFile.exists()) {
- mService = createService();
+ mService = createRefactoredBackupManagerService();
} else {
Slog.i(TAG, "Backup inactive in user " + whichUser);
}
@@ -182,7 +162,7 @@ public class Trampoline extends IBackupManager.Stub {
Slog.i(TAG, "Making backup "
+ (makeActive ? "" : "in") + "active in user " + userHandle);
if (makeActive) {
- mService = createService();
+ mService = createRefactoredBackupManagerService();
mSuppressFile.delete();
} else {
mService = null;
@@ -378,6 +358,26 @@ public class Trampoline extends IBackupManager.Stub {
}
@Override
+ public void updateTransportAttributes(
+ ComponentName transportComponent,
+ String name,
+ @Nullable Intent configurationIntent,
+ String currentDestinationString,
+ @Nullable Intent dataManagementIntent,
+ String dataManagementLabel) {
+ BackupManagerServiceInterface svc = mService;
+ if (svc != null) {
+ svc.updateTransportAttributes(
+ transportComponent,
+ name,
+ configurationIntent,
+ currentDestinationString,
+ dataManagementIntent,
+ dataManagementLabel);
+ }
+ }
+
+ @Override
public String selectBackupTransport(String transport) throws RemoteException {
BackupManagerServiceInterface svc = mService;
return (svc != null) ? svc.selectBackupTransport(transport) : null;
diff --git a/com/android/server/backup/TransportManager.java b/com/android/server/backup/TransportManager.java
index a2b5cb88..fbdb1832 100644
--- a/com/android/server/backup/TransportManager.java
+++ b/com/android/server/backup/TransportManager.java
@@ -16,9 +16,10 @@
package com.android.server.backup;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
import android.annotation.Nullable;
import android.app.backup.BackupManager;
-import android.app.backup.BackupTransport;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -48,12 +49,15 @@ import com.android.server.EventLogTags;
import com.android.server.backup.transport.TransportClient;
import com.android.server.backup.transport.TransportClientManager;
import com.android.server.backup.transport.TransportConnectionListener;
+import com.android.server.backup.transport.TransportNotRegisteredException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
/**
* Handles in-memory bookkeeping of all BackupTransport objects.
@@ -80,11 +84,8 @@ public class TransportManager {
* This listener is called after we bind to any transport. If it returns true, this is a valid
* transport.
*/
- private final TransportBoundListener mTransportBoundListener;
-
- private String mCurrentTransportName;
+ private TransportBoundListener mTransportBoundListener;
- /** Lock on this before accessing mValidTransports and mBoundTransports. */
private final Object mTransportLock = new Object();
/**
@@ -98,9 +99,18 @@ public class TransportManager {
@GuardedBy("mTransportLock")
private final Map<String, ComponentName> mBoundTransports = new ArrayMap<>();
- /** Names of transports we've bound to at least once */
+ /** @see #getEligibleTransportComponents() */
+ @GuardedBy("mTransportLock")
+ private final Set<ComponentName> mEligibleTransports = new ArraySet<>();
+
+ /** @see #getRegisteredTransportNames() */
@GuardedBy("mTransportLock")
- private final Map<String, ComponentName> mTransportsByName = new ArrayMap<>();
+ private final Map<ComponentName, TransportDescription> mRegisteredTransportsDescriptionMap =
+ new ArrayMap<>();
+
+ @GuardedBy("mTransportLock")
+ private volatile String mCurrentTransportName;
+
/**
* Callback interface for {@link #ensureTransportReady(ComponentName, TransportReadyCallback)}.
@@ -118,8 +128,21 @@ public class TransportManager {
void onFailure(int reason);
}
- TransportManager(Context context, Set<ComponentName> whitelist, String defaultTransport,
- TransportBoundListener listener, Looper looper) {
+ TransportManager(
+ Context context,
+ Set<ComponentName> whitelist,
+ String defaultTransport,
+ TransportBoundListener listener,
+ Looper looper) {
+ this(context, whitelist, defaultTransport, looper);
+ mTransportBoundListener = listener;
+ }
+
+ TransportManager(
+ Context context,
+ Set<ComponentName> whitelist,
+ String defaultTransport,
+ Looper looper) {
mContext = context;
mPackageManager = context.getPackageManager();
if (whitelist != null) {
@@ -128,11 +151,14 @@ public class TransportManager {
mTransportWhitelist = new ArraySet<>();
}
mCurrentTransportName = defaultTransport;
- mTransportBoundListener = listener;
mHandler = new RebindOnTimeoutHandler(looper);
mTransportClientManager = new TransportClientManager(context);
}
+ public void setTransportBoundListener(TransportBoundListener transportBoundListener) {
+ mTransportBoundListener = transportBoundListener;
+ }
+
void onPackageAdded(String packageName) {
// New package added. Bind to all transports it contains.
synchronized (mTransportLock) {
@@ -161,6 +187,8 @@ public class TransportManager {
}
}
}
+ removeTransportsIfLocked(
+ componentName -> packageName.equals(componentName.getPackageName()));
}
}
@@ -168,8 +196,10 @@ public class TransportManager {
synchronized (mTransportLock) {
// Remove all changed components from mValidTransports. We'll bind to them again
// and re-add them if still valid.
+ Set<ComponentName> transportsToBeRemoved = new ArraySet<>();
for (String component : components) {
ComponentName componentName = new ComponentName(packageName, component);
+ transportsToBeRemoved.add(componentName);
TransportConnection removed = mValidTransports.remove(componentName);
if (removed != null) {
mContext.unbindService(removed);
@@ -177,10 +207,17 @@ public class TransportManager {
componentName.flattenToShortString());
}
}
+ removeTransportsIfLocked(transportsToBeRemoved::contains);
bindToAllInternal(packageName, components);
}
}
+ @GuardedBy("mTransportLock")
+ private void removeTransportsIfLocked(Predicate<ComponentName> filter) {
+ mEligibleTransports.removeIf(filter);
+ mRegisteredTransportsDescriptionMap.keySet().removeIf(filter);
+ }
+
public IBackupTransport getTransportBinder(String transportName) {
synchronized (mTransportLock) {
ComponentName component = mBoundTransports.get(transportName);
@@ -201,6 +238,72 @@ public class TransportManager {
return getTransportBinder(mCurrentTransportName);
}
+ /**
+ * Retrieve the configuration intent of {@code transportName}.
+ * @throws TransportNotRegisteredException if the transport is not registered.
+ */
+ @Nullable
+ public Intent getTransportConfigurationIntent(String transportName)
+ throws TransportNotRegisteredException {
+ synchronized (mTransportLock) {
+ return getRegisteredTransportDescriptionOrThrowLocked(transportName)
+ .configurationIntent;
+ }
+ }
+
+ /**
+ * Retrieve the data management intent of {@code transportName}.
+ * @throws TransportNotRegisteredException if the transport is not registered.
+ */
+ @Nullable
+ public Intent getTransportDataManagementIntent(String transportName)
+ throws TransportNotRegisteredException {
+ synchronized (mTransportLock) {
+ return getRegisteredTransportDescriptionOrThrowLocked(transportName)
+ .dataManagementIntent;
+ }
+ }
+
+ /**
+ * Retrieve the data management label of {@code transportName}.
+ * @throws TransportNotRegisteredException if the transport is not registered.
+ */
+ @Nullable
+ public String getTransportDataManagementLabel(String transportName)
+ throws TransportNotRegisteredException {
+ synchronized (mTransportLock) {
+ return getRegisteredTransportDescriptionOrThrowLocked(transportName)
+ .dataManagementLabel;
+ }
+ }
+
+ /**
+ * Retrieve the transport dir name of {@code transportName}.
+ * @throws TransportNotRegisteredException if the transport is not registered.
+ */
+ public String getTransportDirName(String transportName)
+ throws TransportNotRegisteredException {
+ synchronized (mTransportLock) {
+ return getRegisteredTransportDescriptionOrThrowLocked(transportName)
+ .transportDirName;
+ }
+ }
+
+ /**
+ * Execute {@code transportConsumer} for each registered transport passing the transport name.
+ * This is called with an internal lock held, ensuring that the transport will remain registered
+ * while {@code transportConsumer} is being executed. Don't do heavy operations in
+ * {@code transportConsumer}.
+ */
+ public void forEachRegisteredTransport(Consumer<String> transportConsumer) {
+ synchronized (mTransportLock) {
+ for (TransportDescription transportDescription
+ : mRegisteredTransportsDescriptionMap.values()) {
+ transportConsumer.accept(transportDescription.name);
+ }
+ }
+ }
+
public String getTransportName(IBackupTransport binder) {
synchronized (mTransportLock) {
for (TransportConnection conn : mValidTransports.values()) {
@@ -213,39 +316,81 @@ public class TransportManager {
}
/**
- * Returns the transport name associated with {@param transportClient} or {@code null} if not
+ * Returns the transport name associated with {@param transportComponent} or {@code null} if not
* found.
*/
@Nullable
- public String getTransportName(TransportClient transportClient) {
- ComponentName transportComponent = transportClient.getTransportComponent();
+ public String getTransportName(ComponentName transportComponent) {
synchronized (mTransportLock) {
- for (Map.Entry<String, ComponentName> transportEntry : mTransportsByName.entrySet()) {
- if (transportEntry.getValue().equals(transportComponent)) {
- return transportEntry.getKey();
- }
+ TransportDescription description =
+ mRegisteredTransportsDescriptionMap.get(transportComponent);
+ if (description == null) {
+ Slog.e(TAG, "Trying to find name of unregistered transport " + transportComponent);
+ return null;
}
- return null;
+ return description.name;
}
}
- /**
- * Returns a {@link TransportClient} for {@param transportName} or {@code null} if not found.
- *
- * @param transportName The name of the transport as returned by {@link BackupTransport#name()}.
- * @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
- * {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
- * details.
- * @return A {@link TransportClient} or null if not found.
- */
+ @GuardedBy("mTransportLock")
+ @Nullable
+ private ComponentName getRegisteredTransportComponentLocked(String transportName) {
+ Map.Entry<ComponentName, TransportDescription> entry =
+ getRegisteredTransportEntryLocked(transportName);
+ return (entry == null) ? null : entry.getKey();
+ }
+
+ @GuardedBy("mTransportLock")
+ @Nullable
+ private TransportDescription getRegisteredTransportDescriptionLocked(String transportName) {
+ Map.Entry<ComponentName, TransportDescription> entry =
+ getRegisteredTransportEntryLocked(transportName);
+ return (entry == null) ? null : entry.getValue();
+ }
+
+ @GuardedBy("mTransportLock")
+ private TransportDescription getRegisteredTransportDescriptionOrThrowLocked(
+ String transportName) throws TransportNotRegisteredException {
+ TransportDescription description = getRegisteredTransportDescriptionLocked(transportName);
+ if (description == null) {
+ throw new TransportNotRegisteredException(transportName);
+ }
+ return description;
+ }
+
+
+ @GuardedBy("mTransportLock")
+ @Nullable
+ private Map.Entry<ComponentName, TransportDescription> getRegisteredTransportEntryLocked(
+ String transportName) {
+ for (Map.Entry<ComponentName, TransportDescription> entry
+ : mRegisteredTransportsDescriptionMap.entrySet()) {
+ TransportDescription description = entry.getValue();
+ if (transportName.equals(description.name)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
@Nullable
public TransportClient getTransportClient(String transportName, String caller) {
- ComponentName transportComponent = mTransportsByName.get(transportName);
- if (transportComponent == null) {
- Slog.w(TAG, "Transport " + transportName + " not registered");
- return null;
+ synchronized (mTransportLock) {
+ ComponentName component = getRegisteredTransportComponentLocked(transportName);
+ if (component == null) {
+ Slog.w(TAG, "Transport " + transportName + " not registered");
+ return null;
+ }
+ TransportDescription description = mRegisteredTransportsDescriptionMap.get(component);
+ return mTransportClientManager.getTransportClient(
+ component, description.transportDirName, caller);
+ }
+ }
+
+ public boolean isTransportRegistered(String transportName) {
+ synchronized (mTransportLock) {
+ return getRegisteredTransportEntryLocked(transportName) != null;
}
- return mTransportClientManager.getTransportClient(transportComponent, caller);
}
/**
@@ -285,14 +430,68 @@ public class TransportManager {
}
}
- String getCurrentTransportName() {
- return mCurrentTransportName;
+ /**
+ * An *eligible* transport is a service component that satisfies intent with action
+ * android.backup.TRANSPORT_HOST and returns true for
+ * {@link #isTransportTrusted(ComponentName)}. It may be registered or not registered.
+ * This method returns the {@link ComponentName}s of those transports.
+ */
+ ComponentName[] getEligibleTransportComponents() {
+ synchronized (mTransportLock) {
+ return mEligibleTransports.toArray(new ComponentName[mEligibleTransports.size()]);
+ }
}
Set<ComponentName> getTransportWhitelist() {
return mTransportWhitelist;
}
+ /**
+ * A *registered* transport is an eligible transport that has been successfully connected and
+ * that returned true for method
+ * {@link TransportBoundListener#onTransportBound(IBackupTransport)} of TransportBoundListener
+ * provided in the constructor. This method returns the names of the registered transports.
+ */
+ String[] getRegisteredTransportNames() {
+ synchronized (mTransportLock) {
+ return mRegisteredTransportsDescriptionMap.values().stream()
+ .map(transportDescription -> transportDescription.name)
+ .toArray(String[]::new);
+ }
+ }
+
+ /**
+ * Updates given values for the transport already registered and identified with
+ * {@param transportComponent}. If the transport is not registered it will log and return.
+ */
+ public void updateTransportAttributes(
+ ComponentName transportComponent,
+ String name,
+ @Nullable Intent configurationIntent,
+ String currentDestinationString,
+ @Nullable Intent dataManagementIntent,
+ @Nullable String dataManagementLabel) {
+ synchronized (mTransportLock) {
+ TransportDescription description =
+ mRegisteredTransportsDescriptionMap.get(transportComponent);
+ if (description == null) {
+ Slog.e(TAG, "Transport " + name + " not registered tried to change description");
+ return;
+ }
+ description.name = name;
+ description.configurationIntent = configurationIntent;
+ description.currentDestinationString = currentDestinationString;
+ description.dataManagementIntent = dataManagementIntent;
+ description.dataManagementLabel = dataManagementLabel;
+ Slog.d(TAG, "Transport " + name + " updated its attributes");
+ }
+ }
+
+ @Nullable
+ String getCurrentTransportName() {
+ return mCurrentTransportName;
+ }
+
String selectTransport(String transport) {
synchronized (mTransportLock) {
String prevTransport = mCurrentTransportName;
@@ -315,7 +514,12 @@ public class TransportManager {
}
}
- void registerAllTransports() {
+
+ // This is for mocking, Mockito can't mock if package-protected and in the same package but
+ // different class loaders. Checked with the debugger and class loaders are different
+ // See https://github.com/mockito/mockito/issues/796
+ @VisibleForTesting(visibility = PACKAGE)
+ public void registerAllTransports() {
bindToAllInternal(null /* all packages */, null /* all components */);
}
@@ -391,6 +595,9 @@ public class TransportManager {
Slog.d(TAG, "Binding to transport: " + transportComponentName.flattenToShortString());
// TODO: b/22388012 (Multi user backup and restore)
TransportConnection connection = new TransportConnection(transportComponentName);
+ synchronized (mTransportLock) {
+ mEligibleTransports.add(transportComponentName);
+ }
if (bindToTransport(transportComponentName, connection)) {
synchronized (mTransportLock) {
mValidTransports.put(transportComponentName, connection);
@@ -407,9 +614,25 @@ public class TransportManager {
createSystemUserHandle());
}
+ /** If {@link RemoteException} is thrown the transport is guaranteed to not be registered. */
+ private void registerTransport(ComponentName transportComponent, IBackupTransport transport)
+ throws RemoteException {
+ synchronized (mTransportLock) {
+ String name = transport.name();
+ TransportDescription description = new TransportDescription(
+ name,
+ transport.transportDirName(),
+ transport.configurationIntent(),
+ transport.currentDestinationString(),
+ transport.dataManagementIntent(),
+ transport.dataManagementLabel());
+ mRegisteredTransportsDescriptionMap.put(transportComponent, description);
+ }
+ }
+
private class TransportConnection implements ServiceConnection {
- // Hold mTransportsLock to access these fields so as to provide a consistent view of them.
+ // Hold mTransportLock to access these fields so as to provide a consistent view of them.
private volatile IBackupTransport mBinder;
private final List<TransportReadyCallback> mListeners = new ArrayList<>();
private volatile String mTransportName;
@@ -433,7 +656,22 @@ public class TransportManager {
mTransportName = mBinder.name();
// BackupManager requests some fields from the transport. If they are
// invalid, throw away this transport.
- success = mTransportBoundListener.onTransportBound(mBinder);
+ final boolean valid;
+ if (mTransportBoundListener != null) {
+ valid = mTransportBoundListener.onTransportBound(mBinder);
+ } else {
+ Slog.w(TAG, "setTransportBoundListener() not called, assuming transport "
+ + component + " valid");
+ valid = true;
+ }
+ if (valid) {
+ // We're now using the always-bound connection to do the registration but
+ // when we remove the always-bound code this will be in the first binding
+ // TODO: Move registration to first binding
+ registerTransport(component, mBinder);
+ // If registerTransport() hasn't thrown...
+ success = true;
+ }
} catch (RemoteException e) {
success = false;
Slog.e(TAG, "Couldn't get transport name.", e);
@@ -443,7 +681,6 @@ public class TransportManager {
String componentShortString = component.flattenToShortString().intern();
if (success) {
Slog.d(TAG, "Bound to transport: " + componentShortString);
- mTransportsByName.put(mTransportName, component);
mBoundTransports.put(mTransportName, component);
for (TransportReadyCallback listener : mListeners) {
listener.onSuccess(mTransportName);
@@ -457,6 +694,7 @@ public class TransportManager {
componentShortString, 0);
mContext.unbindService(this);
mValidTransports.remove(component);
+ mEligibleTransports.remove(component);
mBinder = null;
for (TransportReadyCallback listener : mListeners) {
listener.onFailure(BackupManager.ERROR_TRANSPORT_INVALID);
@@ -601,4 +839,28 @@ public class TransportManager {
public static UserHandle createSystemUserHandle() {
return new UserHandle(UserHandle.USER_SYSTEM);
}
+
+ private static class TransportDescription {
+ private String name;
+ private final String transportDirName;
+ @Nullable private Intent configurationIntent;
+ private String currentDestinationString;
+ @Nullable private Intent dataManagementIntent;
+ @Nullable private String dataManagementLabel;
+
+ private TransportDescription(
+ String name,
+ String transportDirName,
+ @Nullable Intent configurationIntent,
+ String currentDestinationString,
+ @Nullable Intent dataManagementIntent,
+ @Nullable String dataManagementLabel) {
+ this.name = name;
+ this.transportDirName = transportDirName;
+ this.configurationIntent = configurationIntent;
+ this.currentDestinationString = currentDestinationString;
+ this.dataManagementIntent = dataManagementIntent;
+ this.dataManagementLabel = dataManagementLabel;
+ }
+ }
}
diff --git a/com/android/server/backup/TransportManagerTest.java b/com/android/server/backup/TransportManagerTest.java
index 2824b356..82830fe5 100644
--- a/com/android/server/backup/TransportManagerTest.java
+++ b/com/android/server/backup/TransportManagerTest.java
@@ -19,7 +19,11 @@ package com.android.server.backup;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.robolectric.shadow.api.Shadow.extract;
+import static org.testng.Assert.expectThrows;
+import android.annotation.Nullable;
import android.app.backup.BackupManager;
import android.content.ComponentName;
import android.content.Intent;
@@ -28,26 +32,30 @@ import android.content.pm.PackageInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.IBinder;
+import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
-import com.android.server.backup.testing.BackupTransportStub;
-import com.android.server.backup.testing.DefaultPackageManagerWithQueryIntentServicesAsUser;
+import com.android.internal.backup.IBackupTransport;
import com.android.server.backup.testing.ShadowBackupTransportStub;
import com.android.server.backup.testing.ShadowContextImplForBackup;
+import com.android.server.backup.testing.ShadowPackageManagerForBackup;
import com.android.server.backup.testing.TransportBoundListenerStub;
import com.android.server.backup.testing.TransportReadyCallbackStub;
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportNotRegisteredException;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderClasses;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
-import org.robolectric.res.builder.RobolectricPackageManager;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowPackageManager;
import java.util.ArrayList;
import java.util.Arrays;
@@ -55,15 +63,17 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
-@RunWith(RobolectricTestRunner.class)
+@RunWith(FrameworkRobolectricTestRunner.class)
@Config(
manifest = Config.NONE,
- sdk = 23,
+ sdk = 26,
shadows = {
ShadowContextImplForBackup.class,
- ShadowBackupTransportStub.class
+ ShadowBackupTransportStub.class,
+ ShadowPackageManagerForBackup.class
}
)
+@SystemLoaderClasses({TransportManager.class})
@Presubmit
public class TransportManagerTest {
private static final String PACKAGE_NAME = "some.package.name";
@@ -72,7 +82,7 @@ public class TransportManagerTest {
private TransportInfo mTransport1;
private TransportInfo mTransport2;
- private RobolectricPackageManager mPackageManager;
+ private ShadowPackageManager mPackageManagerShadow;
private final TransportBoundListenerStub mTransportBoundListenerStub =
new TransportBoundListenerStub(true);
@@ -85,19 +95,34 @@ public class TransportManagerTest {
MockitoAnnotations.initMocks(this);
ShadowLog.stream = System.out;
- mPackageManager = new DefaultPackageManagerWithQueryIntentServicesAsUser(
- RuntimeEnvironment.getAppResourceLoader());
- RuntimeEnvironment.setRobolectricPackageManager(mPackageManager);
- mTransport1 = new TransportInfo(PACKAGE_NAME, "transport1.name");
- mTransport2 = new TransportInfo(PACKAGE_NAME, "transport2.name");
+ mPackageManagerShadow =
+ (ShadowPackageManagerForBackup)
+ extract(RuntimeEnvironment.application.getPackageManager());
+
+ mTransport1 = new TransportInfo(
+ PACKAGE_NAME,
+ "transport1.name",
+ new Intent(),
+ "currentDestinationString",
+ new Intent(),
+ "dataManagementLabel");
+ mTransport2 = new TransportInfo(
+ PACKAGE_NAME,
+ "transport2.name",
+ new Intent(),
+ "currentDestinationString",
+ new Intent(),
+ "dataManagementLabel");
ShadowContextImplForBackup.sComponentBinderMap.put(mTransport1.componentName,
mTransport1.binder);
ShadowContextImplForBackup.sComponentBinderMap.put(mTransport2.componentName,
mTransport2.binder);
- ShadowBackupTransportStub.sBinderTransportMap.put(mTransport1.binder, mTransport1.stub);
- ShadowBackupTransportStub.sBinderTransportMap.put(mTransport2.binder, mTransport2.stub);
+ ShadowBackupTransportStub.sBinderTransportMap.put(
+ mTransport1.binder, mTransport1.binderInterface);
+ ShadowBackupTransportStub.sBinderTransportMap.put(
+ mTransport2.binder, mTransport2.binderInterface);
}
@After
@@ -123,8 +148,10 @@ public class TransportManagerTest {
Arrays.asList(mTransport1.componentName, mTransport2.componentName));
assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.stub)).isTrue();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.stub)).isTrue();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
+ .isTrue();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
+ .isTrue();
}
@Test
@@ -147,8 +174,10 @@ public class TransportManagerTest {
Collections.singleton(mTransport2.componentName));
assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
Collections.singleton(mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.stub)).isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.stub)).isTrue();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
+ .isFalse();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
+ .isTrue();
}
@Test
@@ -187,8 +216,10 @@ public class TransportManagerTest {
Collections.singleton(mTransport2.componentName));
assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
Collections.singleton(mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.stub)).isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.stub)).isTrue();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
+ .isFalse();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
+ .isTrue();
}
@Test
@@ -244,8 +275,10 @@ public class TransportManagerTest {
Arrays.asList(mTransport1.componentName, mTransport2.componentName));
assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.stub)).isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.stub)).isTrue();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
+ .isFalse();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
+ .isTrue();
}
@Test
@@ -259,8 +292,10 @@ public class TransportManagerTest {
Arrays.asList(mTransport1.componentName, mTransport2.componentName));
assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.stub)).isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.stub)).isFalse();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
+ .isFalse();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
+ .isFalse();
}
@Test
@@ -274,8 +309,10 @@ public class TransportManagerTest {
Arrays.asList(mTransport1.componentName, mTransport2.componentName));
assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.stub)).isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.stub)).isFalse();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
+ .isFalse();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
+ .isFalse();
}
@Test
@@ -289,8 +326,10 @@ public class TransportManagerTest {
Arrays.asList(mTransport1.componentName, mTransport2.componentName));
assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.stub)).isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.stub)).isTrue();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
+ .isFalse();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
+ .isTrue();
}
@Test
@@ -299,9 +338,9 @@ public class TransportManagerTest {
Arrays.asList(mTransport1, mTransport2), mTransport1.name);
assertThat(transportManager.getTransportBinder(mTransport1.name)).isEqualTo(
- mTransport1.stub);
+ mTransport1.binderInterface);
assertThat(transportManager.getTransportBinder(mTransport2.name)).isEqualTo(
- mTransport2.stub);
+ mTransport2.binderInterface);
}
@Test
@@ -320,7 +359,7 @@ public class TransportManagerTest {
assertThat(transportManager.getTransportBinder(mTransport1.name)).isNull();
assertThat(transportManager.getTransportBinder(mTransport2.name)).isEqualTo(
- mTransport2.stub);
+ mTransport2.binderInterface);
}
@Test
@@ -350,7 +389,8 @@ public class TransportManagerTest {
TransportManager transportManager = createTransportManagerAndSetUpTransports(
Arrays.asList(mTransport1, mTransport2), mTransport1.name);
- assertThat(transportManager.getCurrentTransportBinder()).isEqualTo(mTransport1.stub);
+ assertThat(transportManager.getCurrentTransportBinder())
+ .isEqualTo(mTransport1.binderInterface);
}
@Test
@@ -369,8 +409,10 @@ public class TransportManagerTest {
TransportManager transportManager = createTransportManagerAndSetUpTransports(
Arrays.asList(mTransport1, mTransport2), mTransport1.name);
- assertThat(transportManager.getTransportName(mTransport1.stub)).isEqualTo(mTransport1.name);
- assertThat(transportManager.getTransportName(mTransport2.stub)).isEqualTo(mTransport2.name);
+ assertThat(transportManager.getTransportName(mTransport1.binderInterface))
+ .isEqualTo(mTransport1.name);
+ assertThat(transportManager.getTransportName(mTransport2.binderInterface))
+ .isEqualTo(mTransport2.name);
}
@Test
@@ -379,8 +421,9 @@ public class TransportManagerTest {
createTransportManagerAndSetUpTransports(Collections.singletonList(mTransport2),
Collections.singletonList(mTransport1), mTransport1.name);
- assertThat(transportManager.getTransportName(mTransport1.stub)).isNull();
- assertThat(transportManager.getTransportName(mTransport2.stub)).isEqualTo(mTransport2.name);
+ assertThat(transportManager.getTransportName(mTransport1.binderInterface)).isNull();
+ assertThat(transportManager.getTransportName(mTransport2.binderInterface))
+ .isEqualTo(mTransport2.name);
}
@Test
@@ -475,6 +518,116 @@ public class TransportManagerTest {
assertThat(mTransportReadyCallbackStub.getFailureCalls()).isEmpty();
}
+ @Test
+ public void getTransportClient_forRegisteredTransport_returnCorrectly() throws Exception {
+ TransportManager transportManager =
+ createTransportManagerAndSetUpTransports(
+ Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+
+ TransportClient transportClient =
+ transportManager.getTransportClient(mTransport1.name, "caller");
+
+ assertThat(transportClient.getTransportComponent()).isEqualTo(mTransport1.componentName);
+ }
+
+ @Test
+ public void getTransportClient_forOldNameOfTransportThatChangedName_returnsNull()
+ throws Exception {
+ TransportManager transportManager =
+ createTransportManagerAndSetUpTransports(
+ Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ transportManager.updateTransportAttributes(
+ mTransport1.componentName, "newName", null, "destinationString", null, null);
+
+ TransportClient transportClient =
+ transportManager.getTransportClient(mTransport1.name, "caller");
+
+ assertThat(transportClient).isNull();
+ }
+
+ @Test
+ public void getTransportClient_forNewNameOfTransportThatChangedName_returnsCorrectly()
+ throws Exception {
+ TransportManager transportManager =
+ createTransportManagerAndSetUpTransports(
+ Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ transportManager.updateTransportAttributes(
+ mTransport1.componentName, "newName", null, "destinationString", null, null);
+
+ TransportClient transportClient =
+ transportManager.getTransportClient("newName", "caller");
+
+ assertThat(transportClient.getTransportComponent()).isEqualTo(mTransport1.componentName);
+ }
+
+ @Test
+ public void getTransportName_forTransportThatChangedName_returnsNewName()
+ throws Exception {
+ TransportManager transportManager =
+ createTransportManagerAndSetUpTransports(
+ Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ transportManager.updateTransportAttributes(
+ mTransport1.componentName, "newName", null, "destinationString", null, null);
+
+ String transportName = transportManager.getTransportName(mTransport1.componentName);
+
+ assertThat(transportName).isEqualTo("newName");
+ }
+
+ @Test
+ public void isTransportRegistered_returnsCorrectly() throws Exception {
+ TransportManager transportManager =
+ createTransportManagerAndSetUpTransports(
+ Collections.singletonList(mTransport1),
+ Collections.singletonList(mTransport2),
+ mTransport1.name);
+
+ assertThat(transportManager.isTransportRegistered(mTransport1.name)).isTrue();
+ assertThat(transportManager.isTransportRegistered(mTransport2.name)).isFalse();
+ }
+
+ @Test
+ public void getTransportAttributes_forRegisteredTransport_returnsCorrectValues()
+ throws Exception {
+ TransportManager transportManager =
+ createTransportManagerAndSetUpTransports(
+ Collections.singletonList(mTransport1),
+ mTransport1.name);
+
+ assertThat(transportManager.getTransportConfigurationIntent(mTransport1.name))
+ .isEqualTo(mTransport1.binderInterface.configurationIntent());
+ assertThat(transportManager.getTransportDataManagementIntent(mTransport1.name))
+ .isEqualTo(mTransport1.binderInterface.dataManagementIntent());
+ assertThat(transportManager.getTransportDataManagementLabel(mTransport1.name))
+ .isEqualTo(mTransport1.binderInterface.dataManagementLabel());
+ assertThat(transportManager.getTransportDirName(mTransport1.name))
+ .isEqualTo(mTransport1.binderInterface.transportDirName());
+ }
+
+ @Test
+ public void getTransportAttributes_forUnregisteredTransport_throws()
+ throws Exception {
+ TransportManager transportManager =
+ createTransportManagerAndSetUpTransports(
+ Collections.singletonList(mTransport1),
+ Collections.singletonList(mTransport2),
+ mTransport1.name);
+
+ expectThrows(
+ TransportNotRegisteredException.class,
+ () -> transportManager.getTransportConfigurationIntent(mTransport2.name));
+ expectThrows(
+ TransportNotRegisteredException.class,
+ () -> transportManager.getTransportDataManagementIntent(
+ mTransport2.name));
+ expectThrows(
+ TransportNotRegisteredException.class,
+ () -> transportManager.getTransportDataManagementLabel(mTransport2.name));
+ expectThrows(
+ TransportNotRegisteredException.class,
+ () -> transportManager.getTransportDirName(mTransport2.name));
+ }
+
private void setUpPackageWithTransports(String packageName, List<TransportInfo> transports,
int flags) throws Exception {
PackageInfo packageInfo = new PackageInfo();
@@ -482,7 +635,7 @@ public class TransportManagerTest {
packageInfo.applicationInfo = new ApplicationInfo();
packageInfo.applicationInfo.privateFlags = flags;
- mPackageManager.addPackage(packageInfo);
+ mPackageManagerShadow.addPackage(packageInfo);
List<ResolveInfo> transportsInfo = new ArrayList<>();
for (TransportInfo transport : transports) {
@@ -496,7 +649,7 @@ public class TransportManagerTest {
Intent intent = new Intent(TransportManager.SERVICE_ACTION_TRANSPORT_HOST);
intent.setPackage(packageName);
- mPackageManager.addResolveInfoForIntent(intent, transportsInfo);
+ mPackageManagerShadow.addResolveInfoForIntent(intent, transportsInfo);
}
private TransportManager createTransportManagerAndSetUpTransports(
@@ -542,10 +695,12 @@ public class TransportManagerTest {
assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
availableTransportsNames);
for (TransportInfo transport : availableTransports) {
- assertThat(mTransportBoundListenerStub.isCalledForTransport(transport.stub)).isTrue();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(transport.binderInterface))
+ .isTrue();
}
for (TransportInfo transport : unavailableTransports) {
- assertThat(mTransportBoundListenerStub.isCalledForTransport(transport.stub)).isFalse();
+ assertThat(mTransportBoundListenerStub.isCalledForTransport(transport.binderInterface))
+ .isFalse();
}
mTransportBoundListenerStub.resetState();
@@ -557,15 +712,32 @@ public class TransportManagerTest {
public final String packageName;
public final String name;
public final ComponentName componentName;
- public final BackupTransportStub stub;
+ public final IBackupTransport binderInterface;
public final IBinder binder;
- TransportInfo(String packageName, String name) {
+ TransportInfo(
+ String packageName,
+ String name,
+ @Nullable Intent configurationIntent,
+ String currentDestinationString,
+ @Nullable Intent dataManagementIntent,
+ String dataManagementLabel) {
this.packageName = packageName;
this.name = name;
this.componentName = new ComponentName(packageName, name);
- this.stub = new BackupTransportStub(name);
this.binder = mock(IBinder.class);
+ IBackupTransport transport = mock(IBackupTransport.class);
+ try {
+ when(transport.name()).thenReturn(name);
+ when(transport.configurationIntent()).thenReturn(configurationIntent);
+ when(transport.currentDestinationString()).thenReturn(currentDestinationString);
+ when(transport.dataManagementIntent()).thenReturn(dataManagementIntent);
+ when(transport.dataManagementLabel()).thenReturn(dataManagementLabel);
+ } catch (RemoteException e) {
+ // Only here to mock methods that throw RemoteException
+ }
+ this.binderInterface = transport;
}
}
+
}
diff --git a/com/android/server/backup/internal/BackupHandler.java b/com/android/server/backup/internal/BackupHandler.java
index 9011b95c..f29a9c2e 100644
--- a/com/android/server/backup/internal/BackupHandler.java
+++ b/com/android/server/backup/internal/BackupHandler.java
@@ -189,7 +189,7 @@ public class BackupHandler extends Handler {
}
task.execute();
} catch (ClassCastException e) {
- Slog.e(TAG, "Invalid backup task in flight, obj=" + msg.obj);
+ Slog.e(TAG, "Invalid backup/restore task in flight, obj=" + msg.obj);
}
break;
}
@@ -229,10 +229,18 @@ public class BackupHandler extends Handler {
RestoreParams params = (RestoreParams) msg.obj;
Slog.d(TAG, "MSG_RUN_RESTORE observer=" + params.observer);
- PerformUnifiedRestoreTask task = new PerformUnifiedRestoreTask(backupManagerService,
- params.transport,
- params.observer, params.monitor, params.token, params.pkgInfo,
- params.pmToken, params.isSystemRestore, params.filterSet);
+ PerformUnifiedRestoreTask task =
+ new PerformUnifiedRestoreTask(
+ backupManagerService,
+ params.transportClient,
+ params.observer,
+ params.monitor,
+ params.token,
+ params.packageInfo,
+ params.pmToken,
+ params.isSystemRestore,
+ params.filterSet,
+ params.listener);
synchronized (backupManagerService.getPendingRestores()) {
if (backupManagerService.isRestoreInProgress()) {
@@ -268,8 +276,13 @@ public class BackupHandler extends Handler {
case MSG_RUN_CLEAR: {
ClearParams params = (ClearParams) msg.obj;
- (new PerformClearTask(backupManagerService, params.transport,
- params.packageInfo)).run();
+ Runnable task =
+ new PerformClearTask(
+ backupManagerService,
+ params.transportClient,
+ params.packageInfo,
+ params.listener);
+ task.run();
break;
}
@@ -280,22 +293,15 @@ public class BackupHandler extends Handler {
break;
}
- case MSG_RETRY_INIT: {
- synchronized (backupManagerService.getQueueLock()) {
- backupManagerService.recordInitPendingLocked(msg.arg1 != 0, (String) msg.obj);
- backupManagerService.getAlarmManager().set(AlarmManager.RTC_WAKEUP,
- System.currentTimeMillis(),
- backupManagerService.getRunInitIntent());
- }
- break;
- }
-
case MSG_RUN_GET_RESTORE_SETS: {
// Like other async operations, this is entered with the wakelock held
RestoreSet[] sets = null;
RestoreGetSetsParams params = (RestoreGetSetsParams) msg.obj;
+ String callerLogString = "BH/MSG_RUN_GET_RESTORE_SETS";
try {
- sets = params.transport.getAvailableRestoreSets();
+ IBackupTransport transport =
+ params.transportClient.connectOrThrow(callerLogString);
+ sets = transport.getAvailableRestoreSets();
// cache the result in the active session
synchronized (params.session) {
params.session.mRestoreSets = sets;
@@ -320,7 +326,7 @@ public class BackupHandler extends Handler {
removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
sendEmptyMessageDelayed(MSG_RESTORE_SESSION_TIMEOUT, TIMEOUT_RESTORE_INTERVAL);
- backupManagerService.getWakelock().release();
+ params.listener.onFinished(callerLogString);
}
break;
}
diff --git a/com/android/server/backup/internal/PerformBackupTask.java b/com/android/server/backup/internal/PerformBackupTask.java
index 5be1b390..a002334d 100644
--- a/com/android/server/backup/internal/PerformBackupTask.java
+++ b/com/android/server/backup/internal/PerformBackupTask.java
@@ -560,8 +560,8 @@ public class PerformBackupTask implements BackupRestoreTask {
}
backupManagerService.addBackupTrace("init required; rerunning");
try {
- final String name = backupManagerService.getTransportManager().getTransportName(
- mTransportClient);
+ final String name = backupManagerService.getTransportManager()
+ .getTransportName(mTransportClient.getTransportComponent());
if (name != null) {
backupManagerService.getPendingInits().add(name);
} else {
@@ -907,7 +907,7 @@ public class PerformBackupTask implements BackupRestoreTask {
mStatus = BackupTransport.TRANSPORT_OK;
long size = 0;
try {
- TransportUtils.checkTransport(transport);
+ TransportUtils.checkTransportNotNull(transport);
size = mBackupDataName.length();
if (size > 0) {
if (mStatus == BackupTransport.TRANSPORT_OK) {
@@ -997,7 +997,7 @@ public class PerformBackupTask implements BackupRestoreTask {
}
if (mAgentBinder != null) {
try {
- TransportUtils.checkTransport(transport);
+ TransportUtils.checkTransportNotNull(transport);
long quota = transport.getBackupQuota(mCurrentPackage.packageName, false);
mAgentBinder.doQuotaExceeded(size, quota);
} catch (Exception e) {
diff --git a/com/android/server/backup/internal/PerformClearTask.java b/com/android/server/backup/internal/PerformClearTask.java
index 7af01eac..84ca59b5 100644
--- a/com/android/server/backup/internal/PerformClearTask.java
+++ b/com/android/server/backup/internal/PerformClearTask.java
@@ -23,46 +23,54 @@ import android.util.Slog;
import com.android.internal.backup.IBackupTransport;
import com.android.server.backup.RefactoredBackupManagerService;
+import com.android.server.backup.transport.TransportClient;
import java.io.File;
public class PerformClearTask implements Runnable {
-
- private RefactoredBackupManagerService backupManagerService;
- IBackupTransport mTransport;
- PackageInfo mPackage;
+ private final RefactoredBackupManagerService mBackupManagerService;
+ private final TransportClient mTransportClient;
+ private final PackageInfo mPackage;
+ private final OnTaskFinishedListener mListener;
PerformClearTask(RefactoredBackupManagerService backupManagerService,
- IBackupTransport transport, PackageInfo packageInfo) {
- this.backupManagerService = backupManagerService;
- mTransport = transport;
+ TransportClient transportClient, PackageInfo packageInfo,
+ OnTaskFinishedListener listener) {
+ mBackupManagerService = backupManagerService;
+ mTransportClient = transportClient;
mPackage = packageInfo;
+ mListener = listener;
}
public void run() {
+ String callerLogString = "PerformClearTask.run()";
+ IBackupTransport transport = null;
try {
// Clear the on-device backup state to ensure a full backup next time
- File stateDir = new File(backupManagerService.getBaseStateDir(),
- mTransport.transportDirName());
+ File stateDir = new File(mBackupManagerService.getBaseStateDir(),
+ mTransportClient.getTransportDirName());
File stateFile = new File(stateDir, mPackage.packageName);
stateFile.delete();
+ transport = mTransportClient.connectOrThrow(callerLogString);
// Tell the transport to remove all the persistent storage for the app
// TODO - need to handle failures
- mTransport.clearBackupData(mPackage);
+ transport.clearBackupData(mPackage);
} catch (Exception e) {
Slog.e(TAG, "Transport threw clearing data for " + mPackage + ": " + e.getMessage());
} finally {
- try {
- // TODO - need to handle failures
- mTransport.finishBackup();
- } catch (Exception e) {
- // Nothing we can do here, alas
- Slog.e(TAG, "Unable to mark clear operation finished: " + e.getMessage());
+ if (transport != null) {
+ try {
+ // TODO - need to handle failures
+ transport.finishBackup();
+ } catch (Exception e) {
+ // Nothing we can do here, alas
+ Slog.e(TAG, "Unable to mark clear operation finished: " + e.getMessage());
+ }
}
-
+ mListener.onFinished(callerLogString);
// Last but not least, release the cpu
- backupManagerService.getWakelock().release();
+ mBackupManagerService.getWakelock().release();
}
}
}
diff --git a/com/android/server/backup/internal/PerformInitializeTask.java b/com/android/server/backup/internal/PerformInitializeTask.java
index 690922fd..c6246981 100644
--- a/com/android/server/backup/internal/PerformInitializeTask.java
+++ b/com/android/server/backup/internal/PerformInitializeTask.java
@@ -18,6 +18,7 @@ package com.android.server.backup.internal;
import static com.android.server.backup.RefactoredBackupManagerService.TAG;
+import android.annotation.Nullable;
import android.app.AlarmManager;
import android.app.backup.BackupTransport;
import android.app.backup.IBackupObserver;
@@ -26,23 +27,63 @@ import android.os.SystemClock;
import android.util.EventLog;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.backup.IBackupTransport;
import com.android.server.EventLogTags;
import com.android.server.backup.RefactoredBackupManagerService;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.transport.TransportClient;
import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+/**
+ * Attempts to call {@link BackupTransport#initializeDevice()} followed by
+ * {@link BackupTransport#finishBackup()} for the transport names passed in with the intent of
+ * wiping backup data from the transport.
+ *
+ * If the transport returns error, it will record the operation as pending and schedule it to run in
+ * a future time according to {@link BackupTransport#requestBackupTime()}. The result status
+ * reported to observers will be the last unsuccessful status reported by the transports. If every
+ * operation was successful then it's {@link BackupTransport#TRANSPORT_OK}.
+ */
public class PerformInitializeTask implements Runnable {
+ private final RefactoredBackupManagerService mBackupManagerService;
+ private final TransportManager mTransportManager;
+ private final String[] mQueue;
+ private final File mBaseStateDir;
+ private final OnTaskFinishedListener mListener;
+ @Nullable private IBackupObserver mObserver;
- private RefactoredBackupManagerService backupManagerService;
- String[] mQueue;
- IBackupObserver mObserver;
+ public PerformInitializeTask(
+ RefactoredBackupManagerService backupManagerService,
+ String[] transportNames,
+ @Nullable IBackupObserver observer,
+ OnTaskFinishedListener listener) {
+ this(
+ backupManagerService,
+ backupManagerService.getTransportManager(),
+ transportNames,
+ observer,
+ listener,
+ backupManagerService.getBaseStateDir());
+ }
- public PerformInitializeTask(RefactoredBackupManagerService backupManagerService,
- String[] transportNames, IBackupObserver observer) {
- this.backupManagerService = backupManagerService;
+ @VisibleForTesting
+ PerformInitializeTask(
+ RefactoredBackupManagerService backupManagerService,
+ TransportManager transportManager,
+ String[] transportNames,
+ @Nullable IBackupObserver observer,
+ OnTaskFinishedListener listener,
+ File baseStateDir) {
+ mBackupManagerService = backupManagerService;
+ mTransportManager = transportManager;
mQueue = transportNames;
mObserver = observer;
+ mListener = listener;
+ mBaseStateDir = baseStateDir;
}
private void notifyResult(String target, int status) {
@@ -67,21 +108,25 @@ public class PerformInitializeTask implements Runnable {
public void run() {
// mWakelock is *acquired* when execution begins here
+ String callerLogString = "PerformInitializeTask.run()";
+ List<TransportClient> transportClientsToDisposeOf = new ArrayList<>(mQueue.length);
int result = BackupTransport.TRANSPORT_OK;
try {
for (String transportName : mQueue) {
- IBackupTransport transport =
- backupManagerService.getTransportManager().getTransportBinder(
- transportName);
- if (transport == null) {
+ TransportClient transportClient =
+ mTransportManager.getTransportClient(transportName, callerLogString);
+ if (transportClient == null) {
Slog.e(TAG, "Requested init for " + transportName + " but not found");
continue;
}
+ transportClientsToDisposeOf.add(transportClient);
Slog.i(TAG, "Initializing (wiping) backup transport storage: " + transportName);
- String transportDirName = transport.transportDirName();
+ String transportDirName = transportClient.getTransportDirName();
EventLog.writeEvent(EventLogTags.BACKUP_START, transportDirName);
long startRealtime = SystemClock.elapsedRealtime();
+
+ IBackupTransport transport = transportClient.connectOrThrow(callerLogString);
int status = transport.initializeDevice();
if (status == BackupTransport.TRANSPORT_OK) {
@@ -93,40 +138,38 @@ public class PerformInitializeTask implements Runnable {
Slog.i(TAG, "Device init successful");
int millis = (int) (SystemClock.elapsedRealtime() - startRealtime);
EventLog.writeEvent(EventLogTags.BACKUP_INITIALIZE);
- backupManagerService
- .resetBackupState(new File(backupManagerService.getBaseStateDir(),
- transportDirName));
+ File stateFileDir = new File(mBaseStateDir, transportDirName);
+ mBackupManagerService.resetBackupState(stateFileDir);
EventLog.writeEvent(EventLogTags.BACKUP_SUCCESS, 0, millis);
- synchronized (backupManagerService.getQueueLock()) {
- backupManagerService.recordInitPendingLocked(false, transportName);
- }
+ mBackupManagerService.recordInitPending(false, transportName, transportDirName);
notifyResult(transportName, BackupTransport.TRANSPORT_OK);
} else {
// If this didn't work, requeue this one and try again
// after a suitable interval
Slog.e(TAG, "Transport error in initializeDevice()");
EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_FAILURE, "(initialize)");
- synchronized (backupManagerService.getQueueLock()) {
- backupManagerService.recordInitPendingLocked(true, transportName);
- }
+ mBackupManagerService.recordInitPending(true, transportName, transportDirName);
notifyResult(transportName, status);
result = status;
// do this via another alarm to make sure of the wakelock states
long delay = transport.requestBackupTime();
Slog.w(TAG, "Init failed on " + transportName + " resched in " + delay);
- backupManagerService.getAlarmManager().set(AlarmManager.RTC_WAKEUP,
+ mBackupManagerService.getAlarmManager().set(
+ AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + delay,
- backupManagerService.getRunInitIntent());
+ mBackupManagerService.getRunInitIntent());
}
}
} catch (Exception e) {
Slog.e(TAG, "Unexpected error performing init", e);
result = BackupTransport.TRANSPORT_ERROR;
} finally {
- // Done; release the wakelock
+ for (TransportClient transportClient : transportClientsToDisposeOf) {
+ mTransportManager.disposeOfTransportClient(transportClient, callerLogString);
+ }
notifyFinished(result);
- backupManagerService.getWakelock().release();
+ mListener.onFinished(callerLogString);
}
}
}
diff --git a/com/android/server/backup/internal/PerformInitializeTaskTest.java b/com/android/server/backup/internal/PerformInitializeTaskTest.java
new file mode 100644
index 00000000..73f1c2fb
--- /dev/null
+++ b/com/android/server/backup/internal/PerformInitializeTaskTest.java
@@ -0,0 +1,411 @@
+/*
+ * 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 com.android.server.backup.internal;
+
+import static android.app.backup.BackupTransport.TRANSPORT_ERROR;
+import static android.app.backup.BackupTransport.TRANSPORT_OK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.app.Application;
+import android.app.PendingIntent;
+import android.app.backup.IBackupObserver;
+import android.os.DeadObjectException;
+import android.platform.test.annotations.Presubmit;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.RefactoredBackupManagerService;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportNotAvailableException;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderClasses;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(manifest = Config.NONE, sdk = 26)
+@SystemLoaderClasses({PerformInitializeTaskTest.class, TransportManager.class})
+@Presubmit
+public class PerformInitializeTaskTest {
+ private static final String[] TRANSPORT_NAMES = {
+ "android/com.android.internal.backup.LocalTransport",
+ "com.google.android.gms/.backup.migrate.service.D2dTransport",
+ "com.google.android.gms/.backup.BackupTransportService"
+ };
+
+ private static final String TRANSPORT_NAME = TRANSPORT_NAMES[0];
+
+ @Mock private RefactoredBackupManagerService mBackupManagerService;
+ @Mock private TransportManager mTransportManager;
+ @Mock private OnTaskFinishedListener mListener;
+ @Mock private IBackupTransport mTransport;
+ @Mock private IBackupObserver mObserver;
+ @Mock private AlarmManager mAlarmManager;
+ @Mock private PendingIntent mRunInitIntent;
+ private File mBaseStateDir;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ Application context = RuntimeEnvironment.application;
+ mBaseStateDir = new File(context.getCacheDir(), "base_state_dir");
+ assertThat(mBaseStateDir.mkdir()).isTrue();
+
+ when(mBackupManagerService.getAlarmManager()).thenReturn(mAlarmManager);
+ when(mBackupManagerService.getRunInitIntent()).thenReturn(mRunInitIntent);
+ }
+
+ @Test
+ public void testRun_callsTransportCorrectly() throws Exception {
+ setUpTransport(TRANSPORT_NAME);
+ configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_OK);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mTransport).initializeDevice();
+ verify(mTransport).finishBackup();
+ }
+
+ @Test
+ public void testRun_callsBackupManagerCorrectly() throws Exception {
+ setUpTransport(TRANSPORT_NAME);
+ configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_OK);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mBackupManagerService)
+ .recordInitPending(false, TRANSPORT_NAME, dirName(TRANSPORT_NAME));
+ verify(mBackupManagerService)
+ .resetBackupState(eq(new File(mBaseStateDir, dirName(TRANSPORT_NAME))));
+ }
+
+ @Test
+ public void testRun_callsObserverAndListenerCorrectly() throws Exception {
+ setUpTransport(TRANSPORT_NAME);
+ configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_OK);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mObserver).onResult(eq(TRANSPORT_NAME), eq(TRANSPORT_OK));
+ verify(mObserver).backupFinished(eq(TRANSPORT_OK));
+ verify(mListener).onFinished(any());
+ }
+
+ @Test
+ public void testRun_whenInitializeDeviceFails() throws Exception {
+ setUpTransport(TRANSPORT_NAME);
+ configureTransport(mTransport, TRANSPORT_ERROR, 0);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mTransport).initializeDevice();
+ verify(mTransport, never()).finishBackup();
+ verify(mBackupManagerService)
+ .recordInitPending(true, TRANSPORT_NAME, dirName(TRANSPORT_NAME));
+ }
+
+ @Test
+ public void testRun_whenInitializeDeviceFails_callsObserverAndListenerCorrectly()
+ throws Exception {
+ setUpTransport(TRANSPORT_NAME);
+ configureTransport(mTransport, TRANSPORT_ERROR, 0);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mObserver).onResult(eq(TRANSPORT_NAME), eq(TRANSPORT_ERROR));
+ verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+ verify(mListener).onFinished(any());
+ }
+
+ @Test
+ public void testRun_whenInitializeDeviceFails_schedulesAlarm() throws Exception {
+ setUpTransport(TRANSPORT_NAME);
+ configureTransport(mTransport, TRANSPORT_ERROR, 0);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mAlarmManager).set(anyInt(), anyLong(), eq(mRunInitIntent));
+ }
+
+ @Test
+ public void testRun_whenFinishBackupFails() throws Exception {
+ setUpTransport(TRANSPORT_NAME);
+ configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_ERROR);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mTransport).initializeDevice();
+ verify(mTransport).finishBackup();
+ verify(mBackupManagerService)
+ .recordInitPending(true, TRANSPORT_NAME, dirName(TRANSPORT_NAME));
+ }
+
+ @Test
+ public void testRun_whenFinishBackupFails_callsObserverAndListenerCorrectly() throws Exception {
+ setUpTransport(TRANSPORT_NAME);
+ configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_ERROR);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mObserver).onResult(eq(TRANSPORT_NAME), eq(TRANSPORT_ERROR));
+ verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+ verify(mListener).onFinished(any());
+ }
+
+ @Test
+ public void testRun_whenFinishBackupFails_schedulesAlarm() throws Exception {
+ setUpTransport(TRANSPORT_NAME);
+ configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_ERROR);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mAlarmManager).set(anyInt(), anyLong(), eq(mRunInitIntent));
+ }
+
+ @Test
+ public void testRun_whenOnlyOneTransportFails() throws Exception {
+ List<TransportData> transports = setUpTransports(TRANSPORT_NAMES[0], TRANSPORT_NAMES[1]);
+ configureTransport(transports.get(0).transportMock, TRANSPORT_ERROR, 0);
+ configureTransport(transports.get(1).transportMock, TRANSPORT_OK, TRANSPORT_OK);
+ PerformInitializeTask performInitializeTask =
+ createPerformInitializeTask(TRANSPORT_NAMES[0], TRANSPORT_NAMES[1]);
+
+ performInitializeTask.run();
+
+ verify(transports.get(1).transportMock).initializeDevice();
+ verify(mObserver).onResult(eq(TRANSPORT_NAMES[0]), eq(TRANSPORT_ERROR));
+ verify(mObserver).onResult(eq(TRANSPORT_NAMES[1]), eq(TRANSPORT_OK));
+ verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+ }
+
+ @Test
+ public void testRun_withMultipleTransports() throws Exception {
+ List<TransportData> transports = setUpTransports(TRANSPORT_NAMES);
+ configureTransport(transports.get(0).transportMock, TRANSPORT_OK, TRANSPORT_OK);
+ configureTransport(transports.get(1).transportMock, TRANSPORT_OK, TRANSPORT_OK);
+ configureTransport(transports.get(2).transportMock, TRANSPORT_OK, TRANSPORT_OK);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAMES);
+
+ performInitializeTask.run();
+
+ for (TransportData transport : transports) {
+ verify(mTransportManager).getTransportClient(eq(transport.transportName), any());
+ verify(mTransportManager)
+ .disposeOfTransportClient(eq(transport.transportClientMock), any());
+ }
+ }
+
+ @Test
+ public void testRun_whenOnlyOneTransportFails_disposesAllTransports() throws Exception {
+ List<TransportData> transports = setUpTransports(TRANSPORT_NAMES[0], TRANSPORT_NAMES[1]);
+ configureTransport(transports.get(0).transportMock, TRANSPORT_ERROR, 0);
+ configureTransport(transports.get(1).transportMock, TRANSPORT_OK, TRANSPORT_OK);
+ PerformInitializeTask performInitializeTask =
+ createPerformInitializeTask(TRANSPORT_NAMES[0], TRANSPORT_NAMES[1]);
+
+ performInitializeTask.run();
+
+ verify(mTransportManager)
+ .disposeOfTransportClient(eq(transports.get(0).transportClientMock), any());
+ verify(mTransportManager)
+ .disposeOfTransportClient(eq(transports.get(1).transportClientMock), any());
+ }
+
+ @Test
+ public void testRun_whenTransportNotRegistered() throws Exception {
+ setUpTransport(new TransportData(TRANSPORT_NAME, null, null));
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mTransportManager, never()).disposeOfTransportClient(any(), any());
+ verify(mObserver, never()).onResult(any(), anyInt());
+ verify(mObserver).backupFinished(eq(TRANSPORT_OK));
+ }
+
+ @Test
+ public void testRun_whenOnlyOneTransportNotRegistered() throws Exception {
+ List<TransportData> transports =
+ setUpTransports(
+ new TransportData(TRANSPORT_NAMES[0], null, null),
+ new TransportData(TRANSPORT_NAMES[1]));
+ String registeredTransportName = transports.get(1).transportName;
+ IBackupTransport registeredTransport = transports.get(1).transportMock;
+ TransportClient registeredTransportClient = transports.get(1).transportClientMock;
+ PerformInitializeTask performInitializeTask =
+ createPerformInitializeTask(TRANSPORT_NAMES[0], TRANSPORT_NAMES[1]);
+
+ performInitializeTask.run();
+
+ verify(registeredTransport).initializeDevice();
+ verify(mTransportManager).disposeOfTransportClient(eq(registeredTransportClient), any());
+ verify(mObserver).onResult(eq(registeredTransportName), eq(TRANSPORT_OK));
+ }
+
+ @Test
+ public void testRun_whenTransportNotAvailable() throws Exception {
+ TransportClient transportClient = mock(TransportClient.class);
+ setUpTransport(new TransportData(TRANSPORT_NAME, null, transportClient));
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mTransportManager).disposeOfTransportClient(eq(transportClient), any());
+ verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+ verify(mListener).onFinished(any());
+ }
+
+ @Test
+ public void testRun_whenTransportThrowsDeadObjectException() throws Exception {
+ TransportClient transportClient = mock(TransportClient.class);
+ setUpTransport(new TransportData(TRANSPORT_NAME, mTransport, transportClient));
+ when(mTransport.initializeDevice()).thenThrow(DeadObjectException.class);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+
+ performInitializeTask.run();
+
+ verify(mTransportManager).disposeOfTransportClient(eq(transportClient), any());
+ verify(mObserver).backupFinished(eq(TRANSPORT_ERROR));
+ verify(mListener).onFinished(any());
+ }
+
+ private PerformInitializeTask createPerformInitializeTask(String... transportNames) {
+ return new PerformInitializeTask(
+ mBackupManagerService,
+ mTransportManager,
+ transportNames,
+ mObserver,
+ mListener,
+ mBaseStateDir);
+ }
+
+ private void configureTransport(
+ IBackupTransport transportMock, int initializeDeviceStatus, int finishBackupStatus)
+ throws Exception {
+ when(transportMock.initializeDevice()).thenReturn(initializeDeviceStatus);
+ when(transportMock.finishBackup()).thenReturn(finishBackupStatus);
+ }
+
+ private List<TransportData> setUpTransports(String... transportNames) throws Exception {
+ return setUpTransports(
+ Arrays.stream(transportNames)
+ .map(TransportData::new)
+ .toArray(TransportData[]::new));
+ }
+
+ /** @see #setUpTransport(TransportData) */
+ private List<TransportData> setUpTransports(TransportData... transports) throws Exception {
+ for (TransportData transport : transports) {
+ setUpTransport(transport);
+ }
+ return Arrays.asList(transports);
+ }
+
+ private void setUpTransport(String transportName) throws Exception {
+ setUpTransport(new TransportData(transportName, mTransport, mock(TransportClient.class)));
+ }
+
+ /**
+ * Configures transport according to {@link TransportData}:
+ *
+ * <ul>
+ * <li>{@link TransportData#transportMock} {@code null} means {@link
+ * TransportClient#connectOrThrow(String)} throws {@link TransportNotAvailableException}.
+ * <li>{@link TransportData#transportClientMock} {@code null} means {@link
+ * TransportManager#getTransportClient(String, String)} returns {@code null}.
+ * </ul>
+ */
+ private void setUpTransport(TransportData transport) throws Exception {
+ String transportName = transport.transportName;
+ String transportDirName = dirName(transportName);
+ IBackupTransport transportMock = transport.transportMock;
+ TransportClient transportClientMock = transport.transportClientMock;
+
+ if (transportMock != null) {
+ when(transportMock.name()).thenReturn(transportName);
+ when(transportMock.transportDirName()).thenReturn(transportDirName);
+ }
+
+ if (transportClientMock != null) {
+ when(transportClientMock.getTransportDirName()).thenReturn(transportDirName);
+ if (transportMock != null) {
+ when(transportClientMock.connectOrThrow(any())).thenReturn(transportMock);
+ } else {
+ when(transportClientMock.connectOrThrow(any()))
+ .thenThrow(TransportNotAvailableException.class);
+ }
+ }
+
+ when(mTransportManager.getTransportClient(eq(transportName), any()))
+ .thenReturn(transportClientMock);
+ }
+
+ private String dirName(String transportName) {
+ return transportName + "_dir_name";
+ }
+
+ private static class TransportData {
+ private final String transportName;
+ @Nullable private final IBackupTransport transportMock;
+ @Nullable private final TransportClient transportClientMock;
+
+ private TransportData(
+ String transportName,
+ @Nullable IBackupTransport transportMock,
+ @Nullable TransportClient transportClientMock) {
+ this.transportName = transportName;
+ this.transportMock = transportMock;
+ this.transportClientMock = transportClientMock;
+ }
+
+ private TransportData(String transportName) {
+ this(transportName, mock(IBackupTransport.class), mock(TransportClient.class));
+ }
+ }
+}
diff --git a/com/android/server/backup/internal/RunInitializeReceiver.java b/com/android/server/backup/internal/RunInitializeReceiver.java
index 1df0bf0c..6c160a33 100644
--- a/com/android/server/backup/internal/RunInitializeReceiver.java
+++ b/com/android/server/backup/internal/RunInitializeReceiver.java
@@ -23,37 +23,41 @@ import static com.android.server.backup.RefactoredBackupManagerService.TAG;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.os.PowerManager;
import android.util.ArraySet;
import android.util.Slog;
import com.android.server.backup.RefactoredBackupManagerService;
public class RunInitializeReceiver extends BroadcastReceiver {
-
- private RefactoredBackupManagerService backupManagerService;
+ private final RefactoredBackupManagerService mBackupManagerService;
public RunInitializeReceiver(RefactoredBackupManagerService backupManagerService) {
- this.backupManagerService = backupManagerService;
+ mBackupManagerService = backupManagerService;
}
public void onReceive(Context context, Intent intent) {
if (RUN_INITIALIZE_ACTION.equals(intent.getAction())) {
- synchronized (backupManagerService.getQueueLock()) {
- final ArraySet<String> pendingInits = backupManagerService.getPendingInits();
+ synchronized (mBackupManagerService.getQueueLock()) {
+ final ArraySet<String> pendingInits = mBackupManagerService.getPendingInits();
if (DEBUG) {
Slog.v(TAG, "Running a device init; " + pendingInits.size() + " pending");
}
if (pendingInits.size() > 0) {
- final String[] transports = pendingInits.toArray(new String[pendingInits.size()]);
- PerformInitializeTask initTask = new PerformInitializeTask(backupManagerService,
- transports, null);
-
- // Acquire the wakelock and pass it to the init thread. it will
- // be released once init concludes.
- backupManagerService.clearPendingInits();
- backupManagerService.getWakelock().acquire();
- backupManagerService.getBackupHandler().post(initTask);
+ final String[] transports =
+ pendingInits.toArray(new String[pendingInits.size()]);
+
+ mBackupManagerService.clearPendingInits();
+
+ PowerManager.WakeLock wakelock = mBackupManagerService.getWakelock();
+ wakelock.acquire();
+ OnTaskFinishedListener listener = caller -> wakelock.release();
+
+ Runnable task =
+ new PerformInitializeTask(
+ mBackupManagerService, transports, null, listener);
+ mBackupManagerService.getBackupHandler().post(task);
}
}
}
diff --git a/com/android/server/backup/params/ClearParams.java b/com/android/server/backup/params/ClearParams.java
index d744efce..dc3bba00 100644
--- a/com/android/server/backup/params/ClearParams.java
+++ b/com/android/server/backup/params/ClearParams.java
@@ -18,15 +18,20 @@ package com.android.server.backup.params;
import android.content.pm.PackageInfo;
-import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.transport.TransportClient;
public class ClearParams {
-
- public IBackupTransport transport;
+ public TransportClient transportClient;
public PackageInfo packageInfo;
+ public OnTaskFinishedListener listener;
- public ClearParams(IBackupTransport _transport, PackageInfo _info) {
- transport = _transport;
- packageInfo = _info;
+ public ClearParams(
+ TransportClient transportClient,
+ PackageInfo packageInfo,
+ OnTaskFinishedListener listener) {
+ this.transportClient = transportClient;
+ this.packageInfo = packageInfo;
+ this.listener = listener;
}
}
diff --git a/com/android/server/backup/params/ClearRetryParams.java b/com/android/server/backup/params/ClearRetryParams.java
index fcf66e40..41b56410 100644
--- a/com/android/server/backup/params/ClearRetryParams.java
+++ b/com/android/server/backup/params/ClearRetryParams.java
@@ -17,12 +17,11 @@
package com.android.server.backup.params;
public class ClearRetryParams {
-
public String transportName;
public String packageName;
- public ClearRetryParams(String transport, String pkg) {
- transportName = transport;
- packageName = pkg;
+ public ClearRetryParams(String transportName, String packageName) {
+ this.transportName = transportName;
+ this.packageName = packageName;
}
}
diff --git a/com/android/server/backup/params/RestoreGetSetsParams.java b/com/android/server/backup/params/RestoreGetSetsParams.java
index bff476be..914e9ea7 100644
--- a/com/android/server/backup/params/RestoreGetSetsParams.java
+++ b/com/android/server/backup/params/RestoreGetSetsParams.java
@@ -20,20 +20,24 @@ import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IRestoreObserver;
import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.internal.OnTaskFinishedListener;
import com.android.server.backup.restore.ActiveRestoreSession;
+import com.android.server.backup.transport.TransportClient;
public class RestoreGetSetsParams {
+ public final TransportClient transportClient;
+ public final ActiveRestoreSession session;
+ public final IRestoreObserver observer;
+ public final IBackupManagerMonitor monitor;
+ public final OnTaskFinishedListener listener;
- public IBackupTransport transport;
- public ActiveRestoreSession session;
- public IRestoreObserver observer;
- public IBackupManagerMonitor monitor;
-
- public RestoreGetSetsParams(IBackupTransport _transport, ActiveRestoreSession _session,
- IRestoreObserver _observer, IBackupManagerMonitor _monitor) {
- transport = _transport;
+ public RestoreGetSetsParams(TransportClient _transportClient, ActiveRestoreSession _session,
+ IRestoreObserver _observer, IBackupManagerMonitor _monitor,
+ OnTaskFinishedListener _listener) {
+ transportClient = _transportClient;
session = _session;
observer = _observer;
monitor = _monitor;
+ listener = _listener;
}
}
diff --git a/com/android/server/backup/params/RestoreParams.java b/com/android/server/backup/params/RestoreParams.java
index 93ce00d8..e500d6e1 100644
--- a/com/android/server/backup/params/RestoreParams.java
+++ b/com/android/server/backup/params/RestoreParams.java
@@ -20,84 +20,128 @@ import android.app.backup.IBackupManagerMonitor;
import android.app.backup.IRestoreObserver;
import android.content.pm.PackageInfo;
-import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.transport.TransportClient;
public class RestoreParams {
-
- public IBackupTransport transport;
- public String dirName;
- public IRestoreObserver observer;
- public IBackupManagerMonitor monitor;
- public long token;
- public PackageInfo pkgInfo;
- public int pmToken; // in post-install restore, the PM's token for this transaction
- public boolean isSystemRestore;
- public String[] filterSet;
+ public final TransportClient transportClient;
+ public final IRestoreObserver observer;
+ public final IBackupManagerMonitor monitor;
+ public final long token;
+ public final PackageInfo packageInfo;
+ public final int pmToken; // in post-install restore, the PM's token for this transaction
+ public final boolean isSystemRestore;
+ public final String[] filterSet;
+ public final OnTaskFinishedListener listener;
/**
- * Restore a single package; no kill after restore
+ * No kill after restore.
*/
- public RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs,
- IBackupManagerMonitor _monitor, long _token, PackageInfo _pkg) {
- transport = _transport;
- dirName = _dirName;
- observer = _obs;
- monitor = _monitor;
- token = _token;
- pkgInfo = _pkg;
- pmToken = 0;
- isSystemRestore = false;
- filterSet = null;
+ public static RestoreParams createForSinglePackage(
+ TransportClient transportClient,
+ IRestoreObserver observer,
+ IBackupManagerMonitor monitor,
+ long token,
+ PackageInfo packageInfo,
+ OnTaskFinishedListener listener) {
+ return new RestoreParams(
+ transportClient,
+ observer,
+ monitor,
+ token,
+ packageInfo,
+ /* pmToken */ 0,
+ /* isSystemRestore */ false,
+ /* filterSet */ null,
+ listener);
}
/**
- * Restore at install: PM token needed, kill after restore
+ * Kill after restore.
*/
- public RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs,
- IBackupManagerMonitor _monitor, long _token, String _pkgName, int _pmToken) {
- transport = _transport;
- dirName = _dirName;
- observer = _obs;
- monitor = _monitor;
- token = _token;
- pkgInfo = null;
- pmToken = _pmToken;
- isSystemRestore = false;
- filterSet = new String[]{_pkgName};
+ public static RestoreParams createForRestoreAtInstall(
+ TransportClient transportClient,
+ IRestoreObserver observer,
+ IBackupManagerMonitor monitor,
+ long token,
+ String packageName,
+ int pmToken,
+ OnTaskFinishedListener listener) {
+ String[] filterSet = {packageName};
+ return new RestoreParams(
+ transportClient,
+ observer,
+ monitor,
+ token,
+ /* packageInfo */ null,
+ pmToken,
+ /* isSystemRestore */ false,
+ filterSet,
+ listener);
}
/**
- * Restore everything possible. This is the form that Setup Wizard or similar
- * restore UXes use.
+ * This is the form that Setup Wizard or similar restore UXes use.
*/
- public RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs,
- IBackupManagerMonitor _monitor, long _token) {
- transport = _transport;
- dirName = _dirName;
- observer = _obs;
- monitor = _monitor;
- token = _token;
- pkgInfo = null;
- pmToken = 0;
- isSystemRestore = true;
- filterSet = null;
+ public static RestoreParams createForRestoreAll(
+ TransportClient transportClient,
+ IRestoreObserver observer,
+ IBackupManagerMonitor monitor,
+ long token,
+ OnTaskFinishedListener listener) {
+ return new RestoreParams(
+ transportClient,
+ observer,
+ monitor,
+ token,
+ /* packageInfo */ null,
+ /* pmToken */ 0,
+ /* isSystemRestore */ true,
+ /* filterSet */ null,
+ listener);
}
/**
- * Restore some set of packages. Leave this one up to the caller to specify
- * whether it's to be considered a system-level restore.
+ * Caller specifies whether is considered a system-level restore.
*/
- public RestoreParams(IBackupTransport _transport, String _dirName, IRestoreObserver _obs,
- IBackupManagerMonitor _monitor, long _token,
- String[] _filterSet, boolean _isSystemRestore) {
- transport = _transport;
- dirName = _dirName;
- observer = _obs;
- monitor = _monitor;
- token = _token;
- pkgInfo = null;
- pmToken = 0;
- isSystemRestore = _isSystemRestore;
- filterSet = _filterSet;
+ public static RestoreParams createForRestoreSome(
+ TransportClient transportClient,
+ IRestoreObserver observer,
+ IBackupManagerMonitor monitor,
+ long token,
+ String[] filterSet,
+ boolean isSystemRestore,
+ OnTaskFinishedListener listener) {
+ return new RestoreParams(
+ transportClient,
+ observer,
+ monitor,
+ token,
+ /* packageInfo */ null,
+ /* pmToken */ 0,
+ isSystemRestore,
+ filterSet,
+ listener);
+ }
+
+ private RestoreParams(
+ TransportClient transportClient,
+ IRestoreObserver observer,
+ IBackupManagerMonitor monitor,
+ long token,
+ PackageInfo packageInfo,
+ int pmToken,
+ boolean isSystemRestore,
+ String[] filterSet,
+ OnTaskFinishedListener listener) {
+ this.transportClient = transportClient;
+ this.observer = observer;
+ this.monitor = monitor;
+ this.token = token;
+ this.packageInfo = packageInfo;
+ this.pmToken = pmToken;
+ this.isSystemRestore = isSystemRestore;
+ this.filterSet = filterSet;
+ this.listener = listener;
}
}
diff --git a/com/android/server/backup/restore/ActiveRestoreSession.java b/com/android/server/backup/restore/ActiveRestoreSession.java
index a08c19e9..7ae5b438 100644
--- a/com/android/server/backup/restore/ActiveRestoreSession.java
+++ b/com/android/server/backup/restore/ActiveRestoreSession.java
@@ -31,33 +31,39 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Binder;
import android.os.Message;
+import android.os.PowerManager;
import android.util.Slog;
-import com.android.internal.backup.IBackupTransport;
import com.android.server.backup.RefactoredBackupManagerService;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.internal.BackupHandler;
+import com.android.server.backup.internal.OnTaskFinishedListener;
import com.android.server.backup.params.RestoreGetSetsParams;
import com.android.server.backup.params.RestoreParams;
+import com.android.server.backup.transport.TransportClient;
+
+import java.util.function.BiFunction;
/**
* Restore session.
*/
public class ActiveRestoreSession extends IRestoreSession.Stub {
-
private static final String TAG = "RestoreSession";
- private RefactoredBackupManagerService backupManagerService;
- private String mPackageName;
- private IBackupTransport mRestoreTransport = null;
+ private final TransportManager mTransportManager;
+ private final String mTransportName;
+ private final RefactoredBackupManagerService mBackupManagerService;
+ private final String mPackageName;
public RestoreSet[] mRestoreSets = null;
boolean mEnded = false;
boolean mTimedOut = false;
public ActiveRestoreSession(RefactoredBackupManagerService backupManagerService,
- String packageName, String transport) {
- this.backupManagerService = backupManagerService;
+ String packageName, String transportName) {
+ mBackupManagerService = backupManagerService;
mPackageName = packageName;
- mRestoreTransport = backupManagerService.getTransportManager().getTransportBinder(
- transport);
+ mTransportManager = backupManagerService.getTransportManager();
+ mTransportName = transportName;
}
public void markTimedOut() {
@@ -67,7 +73,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
// --- Binder interface ---
public synchronized int getAvailableRestoreSets(IRestoreObserver observer,
IBackupManagerMonitor monitor) {
- backupManagerService.getContext().enforceCallingOrSelfPermission(
+ mBackupManagerService.getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.BACKUP,
"getAvailableRestoreSets");
if (observer == null) {
@@ -85,23 +91,32 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
long oldId = Binder.clearCallingIdentity();
try {
- if (mRestoreTransport == null) {
- Slog.w(TAG, "Null transport getting restore sets");
+ TransportClient transportClient =
+ mTransportManager.getTransportClient(
+ mTransportName, "RestoreSession.getAvailableRestoreSets()");
+ if (transportClient == null) {
+ Slog.w(TAG, "Null transport client getting restore sets");
return -1;
}
// We know we're doing legit work now, so halt the timeout
// until we're done. It gets started again when the result
// comes in.
- backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
-
- // spin off the transport request to our service thread
- backupManagerService.getWakelock().acquire();
- Message msg = backupManagerService.getBackupHandler().obtainMessage(
+ mBackupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
+
+ PowerManager.WakeLock wakelock = mBackupManagerService.getWakelock();
+ wakelock.acquire();
+
+ // Prevent lambda from leaking 'this'
+ TransportManager transportManager = mTransportManager;
+ OnTaskFinishedListener listener = caller -> {
+ transportManager.disposeOfTransportClient(transportClient, caller);
+ wakelock.release();
+ };
+ Message msg = mBackupManagerService.getBackupHandler().obtainMessage(
MSG_RUN_GET_RESTORE_SETS,
- new RestoreGetSetsParams(mRestoreTransport, this, observer,
- monitor));
- backupManagerService.getBackupHandler().sendMessage(msg);
+ new RestoreGetSetsParams(transportClient, this, observer, monitor, listener));
+ mBackupManagerService.getBackupHandler().sendMessage(msg);
return 0;
} catch (Exception e) {
Slog.e(TAG, "Error in getAvailableRestoreSets", e);
@@ -113,7 +128,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
public synchronized int restoreAll(long token, IRestoreObserver observer,
IBackupManagerMonitor monitor) {
- backupManagerService.getContext().enforceCallingOrSelfPermission(
+ mBackupManagerService.getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.BACKUP,
"performRestore");
@@ -131,7 +146,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
return -1;
}
- if (mRestoreTransport == null || mRestoreSets == null) {
+ if (mRestoreSets == null) {
Slog.e(TAG, "Ignoring restoreAll() with no restore set");
return -1;
}
@@ -141,37 +156,28 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
return -1;
}
- String dirName;
- try {
- dirName = mRestoreTransport.transportDirName();
- } catch (Exception e) {
- // Transport went AWOL; fail.
- Slog.e(TAG, "Unable to get transport dir for restore: " + e.getMessage());
+ if (!mTransportManager.isTransportRegistered(mTransportName)) {
+ Slog.e(TAG, "Transport " + mTransportName + " not registered");
return -1;
}
- synchronized (backupManagerService.getQueueLock()) {
+ synchronized (mBackupManagerService.getQueueLock()) {
for (int i = 0; i < mRestoreSets.length; i++) {
if (token == mRestoreSets[i].token) {
- // Real work, so stop the session timeout until we finalize the restore
- backupManagerService.getBackupHandler().removeMessages(
- MSG_RESTORE_SESSION_TIMEOUT);
-
long oldId = Binder.clearCallingIdentity();
try {
- backupManagerService.getWakelock().acquire();
- if (MORE_DEBUG) {
- Slog.d(TAG, "restoreAll() kicking off");
- }
- Message msg = backupManagerService.getBackupHandler().obtainMessage(
- MSG_RUN_RESTORE);
- msg.obj = new RestoreParams(mRestoreTransport, dirName,
- observer, monitor, token);
- backupManagerService.getBackupHandler().sendMessage(msg);
+ return sendRestoreToHandlerLocked(
+ (transportClient, listener) ->
+ RestoreParams.createForRestoreAll(
+ transportClient,
+ observer,
+ monitor,
+ token,
+ listener),
+ "RestoreSession.restoreAll()");
} finally {
Binder.restoreCallingIdentity(oldId);
}
- return 0;
}
}
}
@@ -183,7 +189,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
// Restores of more than a single package are treated as 'system' restores
public synchronized int restoreSome(long token, IRestoreObserver observer,
IBackupManagerMonitor monitor, String[] packages) {
- backupManagerService.getContext().enforceCallingOrSelfPermission(
+ mBackupManagerService.getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.BACKUP,
"performRestore");
@@ -227,7 +233,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
return -1;
}
- if (mRestoreTransport == null || mRestoreSets == null) {
+ if (mRestoreSets == null) {
Slog.e(TAG, "Ignoring restoreAll() with no restore set");
return -1;
}
@@ -237,34 +243,30 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
return -1;
}
- String dirName;
- try {
- dirName = mRestoreTransport.transportDirName();
- } catch (Exception e) {
- // Transport went AWOL; fail.
- Slog.e(TAG, "Unable to get transport name for restoreSome: " + e.getMessage());
+ if (!mTransportManager.isTransportRegistered(mTransportName)) {
+ Slog.e(TAG, "Transport " + mTransportName + " not registered");
return -1;
}
- synchronized (backupManagerService.getQueueLock()) {
+ synchronized (mBackupManagerService.getQueueLock()) {
for (int i = 0; i < mRestoreSets.length; i++) {
if (token == mRestoreSets[i].token) {
- // Stop the session timeout until we finalize the restore
- backupManagerService.getBackupHandler().removeMessages(
- MSG_RESTORE_SESSION_TIMEOUT);
-
long oldId = Binder.clearCallingIdentity();
- backupManagerService.getWakelock().acquire();
- if (MORE_DEBUG) {
- Slog.d(TAG, "restoreSome() of " + packages.length + " packages");
+ try {
+ return sendRestoreToHandlerLocked(
+ (transportClient, listener) ->
+ RestoreParams.createForRestoreSome(
+ transportClient,
+ observer,
+ monitor,
+ token,
+ packages,
+ /* isSystemRestore */ packages.length > 1,
+ listener),
+ "RestoreSession.restoreSome(" + packages.length + " packages)");
+ } finally {
+ Binder.restoreCallingIdentity(oldId);
}
- Message msg = backupManagerService.getBackupHandler().obtainMessage(
- MSG_RUN_RESTORE);
- msg.obj = new RestoreParams(mRestoreTransport, dirName, observer, monitor,
- token, packages, packages.length > 1);
- backupManagerService.getBackupHandler().sendMessage(msg);
- Binder.restoreCallingIdentity(oldId);
- return 0;
}
}
}
@@ -297,9 +299,9 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
}
}
- PackageInfo app = null;
+ final PackageInfo app;
try {
- app = backupManagerService.getPackageManager().getPackageInfo(packageName, 0);
+ app = mBackupManagerService.getPackageManager().getPackageInfo(packageName, 0);
} catch (NameNotFoundException nnf) {
Slog.w(TAG, "Asked to restore nonexistent pkg " + packageName);
return -1;
@@ -307,7 +309,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
// If the caller is not privileged and is not coming from the target
// app's uid, throw a permission exception back to the caller.
- int perm = backupManagerService.getContext().checkPermission(
+ int perm = mBackupManagerService.getContext().checkPermission(
android.Manifest.permission.BACKUP,
Binder.getCallingPid(), Binder.getCallingUid());
if ((perm == PackageManager.PERMISSION_DENIED) &&
@@ -317,12 +319,17 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
throw new SecurityException("No permission to restore other packages");
}
+ if (!mTransportManager.isTransportRegistered(mTransportName)) {
+ Slog.e(TAG, "Transport " + mTransportName + " not registered");
+ return -1;
+ }
+
// So far so good; we're allowed to try to restore this package.
long oldId = Binder.clearCallingIdentity();
try {
// Check whether there is data for it in the current dataset, falling back
// to the ancestral dataset if not.
- long token = backupManagerService.getAvailableRestoreToken(packageName);
+ long token = mBackupManagerService.getAvailableRestoreToken(packageName);
if (DEBUG) {
Slog.v(TAG, "restorePackage pkg=" + packageName
+ " token=" + Long.toHexString(token));
@@ -338,30 +345,53 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
return -1;
}
- String dirName;
- try {
- dirName = mRestoreTransport.transportDirName();
- } catch (Exception e) {
- // Transport went AWOL; fail.
- Slog.e(TAG, "Unable to get transport dir for restorePackage: " + e.getMessage());
- return -1;
- }
-
- // Stop the session timeout until we finalize the restore
- backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
-
- // Ready to go: enqueue the restore request and claim success
- backupManagerService.getWakelock().acquire();
- if (MORE_DEBUG) {
- Slog.d(TAG, "restorePackage() : " + packageName);
- }
- Message msg = backupManagerService.getBackupHandler().obtainMessage(MSG_RUN_RESTORE);
- msg.obj = new RestoreParams(mRestoreTransport, dirName, observer, monitor,
- token, app);
- backupManagerService.getBackupHandler().sendMessage(msg);
+ return sendRestoreToHandlerLocked(
+ (transportClient, listener) ->
+ RestoreParams.createForSinglePackage(
+ transportClient,
+ observer,
+ monitor,
+ token,
+ app,
+ listener),
+ "RestoreSession.restorePackage(" + packageName + ")");
} finally {
Binder.restoreCallingIdentity(oldId);
}
+ }
+
+ /**
+ * Returns 0 if operation sent or -1 otherwise.
+ */
+ private int sendRestoreToHandlerLocked(
+ BiFunction<TransportClient, OnTaskFinishedListener, RestoreParams> restoreParamsBuilder,
+ String callerLogString) {
+ TransportClient transportClient =
+ mTransportManager.getTransportClient(mTransportName, callerLogString);
+ if (transportClient == null) {
+ Slog.e(TAG, "Transport " + mTransportName + " got unregistered");
+ return -1;
+ }
+
+ // Stop the session timeout until we finalize the restore
+ BackupHandler backupHandler = mBackupManagerService.getBackupHandler();
+ backupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
+
+ PowerManager.WakeLock wakelock = mBackupManagerService.getWakelock();
+ wakelock.acquire();
+ if (MORE_DEBUG) {
+ Slog.d(TAG, callerLogString);
+ }
+
+ // Prevent lambda from leaking 'this'
+ TransportManager transportManager = mTransportManager;
+ OnTaskFinishedListener listener = caller -> {
+ transportManager.disposeOfTransportClient(transportClient, caller);
+ wakelock.release();
+ };
+ Message msg = backupHandler.obtainMessage(MSG_RUN_RESTORE);
+ msg.obj = restoreParamsBuilder.apply(transportClient, listener);
+ backupHandler.sendMessage(msg);
return 0;
}
@@ -380,7 +410,6 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
public void run() {
// clean up the session's bookkeeping
synchronized (mSession) {
- mSession.mRestoreTransport = null;
mSession.mEnded = true;
}
@@ -404,7 +433,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub {
throw new IllegalStateException("Restore session already ended");
}
- backupManagerService.getBackupHandler().post(
- new EndRestoreRunnable(backupManagerService, this));
+ mBackupManagerService.getBackupHandler().post(
+ new EndRestoreRunnable(mBackupManagerService, this));
}
}
diff --git a/com/android/server/backup/restore/FullRestoreEngine.java b/com/android/server/backup/restore/FullRestoreEngine.java
index 888dcd7e..7efe5cad 100644
--- a/com/android/server/backup/restore/FullRestoreEngine.java
+++ b/com/android/server/backup/restore/FullRestoreEngine.java
@@ -327,7 +327,7 @@ public class FullRestoreEngine extends RestoreEngine {
Slog.d(TAG,
"Clearing app data preparatory to full restore");
}
- mBackupManagerService.clearApplicationDataSynchronous(pkg);
+ mBackupManagerService.clearApplicationDataSynchronous(pkg, true);
} else {
if (MORE_DEBUG) {
Slog.d(TAG, "backup agent ("
diff --git a/com/android/server/backup/restore/PerformAdbRestoreTask.java b/com/android/server/backup/restore/PerformAdbRestoreTask.java
index 22691bb6..3dc242f1 100644
--- a/com/android/server/backup/restore/PerformAdbRestoreTask.java
+++ b/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -591,7 +591,7 @@ public class PerformAdbRestoreTask implements Runnable {
Slog.d(TAG,
"Clearing app data preparatory to full restore");
}
- mBackupManagerService.clearApplicationDataSynchronous(pkg);
+ mBackupManagerService.clearApplicationDataSynchronous(pkg, true);
} else {
if (DEBUG) {
Slog.d(TAG, "backup agent ("
diff --git a/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index b538c6d4..86866dca 100644
--- a/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -61,6 +61,8 @@ import com.android.server.backup.BackupUtils;
import com.android.server.backup.PackageManagerBackupAgent;
import com.android.server.backup.PackageManagerBackupAgent.Metadata;
import com.android.server.backup.RefactoredBackupManagerService;
+import com.android.server.backup.internal.OnTaskFinishedListener;
+import com.android.server.backup.transport.TransportClient;
import com.android.server.backup.utils.AppBackupUtils;
import com.android.server.backup.utils.BackupManagerMonitorUtils;
@@ -76,8 +78,8 @@ import java.util.List;
public class PerformUnifiedRestoreTask implements BackupRestoreTask {
private RefactoredBackupManagerService backupManagerService;
- // Transport we're working with to do the restore
- private IBackupTransport mTransport;
+ // Transport client we're working with to do the restore
+ private final TransportClient mTransportClient;
// Where per-transport saved state goes
File mStateDir;
@@ -141,6 +143,9 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// Done?
private boolean mFinished;
+ // When finished call listener
+ private final OnTaskFinishedListener mListener;
+
// Key/value: bookkeeping about staged data and files for agent access
private File mBackupDataName;
private File mStageName;
@@ -154,15 +159,16 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// Invariant: mWakelock is already held, and this task is responsible for
// releasing it at the end of the restore operation.
public PerformUnifiedRestoreTask(RefactoredBackupManagerService backupManagerService,
- IBackupTransport transport, IRestoreObserver observer,
+ TransportClient transportClient, IRestoreObserver observer,
IBackupManagerMonitor monitor, long restoreSetToken, PackageInfo targetPackage,
- int pmToken, boolean isFullSystemRestore, String[] filterSet) {
+ int pmToken, boolean isFullSystemRestore, String[] filterSet,
+ OnTaskFinishedListener listener) {
this.backupManagerService = backupManagerService;
mEphemeralOpToken = backupManagerService.generateRandomIntegerToken();
mState = UnifiedRestoreState.INITIAL;
mStartRealtime = SystemClock.elapsedRealtime();
- mTransport = transport;
+ mTransportClient = transportClient;
mObserver = observer;
mMonitor = monitor;
mToken = restoreSetToken;
@@ -171,6 +177,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
mIsSystemRestore = isFullSystemRestore;
mFinished = false;
mDidLaunch = false;
+ mListener = listener;
if (targetPackage != null) {
// Single package restore
@@ -342,7 +349,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
}
try {
- String transportDir = mTransport.transportDirName();
+ String transportDir = mTransportClient.getTransportDirName();
mStateDir = new File(backupManagerService.getBaseStateDir(), transportDir);
// Fetch the current metadata from the dataset first
@@ -351,7 +358,11 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
mAcceptSet.add(0, pmPackage);
PackageInfo[] packages = mAcceptSet.toArray(new PackageInfo[0]);
- mStatus = mTransport.startRestore(mToken, packages);
+
+ IBackupTransport transport =
+ mTransportClient.connectOrThrow("PerformUnifiedRestoreTask.startRestore()");
+
+ mStatus = transport.startRestore(mToken, packages);
if (mStatus != BackupTransport.TRANSPORT_OK) {
Slog.e(TAG, "Transport error " + mStatus + "; no restore possible");
mStatus = BackupTransport.TRANSPORT_ERROR;
@@ -359,7 +370,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
return;
}
- RestoreDescription desc = mTransport.nextRestorePackage();
+ RestoreDescription desc = transport.nextRestorePackage();
if (desc == null) {
Slog.e(TAG, "No restore metadata available; halting");
mMonitor = BackupManagerMonitorUtils.monitorEvent(mMonitor,
@@ -444,7 +455,10 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
private void dispatchNextRestore() {
UnifiedRestoreState nextState = UnifiedRestoreState.FINAL;
try {
- mRestoreDescription = mTransport.nextRestorePackage();
+ IBackupTransport transport =
+ mTransportClient.connectOrThrow(
+ "PerformUnifiedRestoreTask.dispatchNextRestore()");
+ mRestoreDescription = transport.nextRestorePackage();
final String pkgName = (mRestoreDescription != null)
? mRestoreDescription.getPackageName() : null;
if (pkgName == null) {
@@ -495,14 +509,14 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
return;
}
- if (metaInfo.versionCode > mCurrentPackage.versionCode) {
+ if (metaInfo.versionCode > mCurrentPackage.getLongVersionCode()) {
// Data is from a "newer" version of the app than we have currently
// installed. If the app has not declared that it is prepared to
// handle this case, we do not attempt the restore.
if ((mCurrentPackage.applicationInfo.flags
& ApplicationInfo.FLAG_RESTORE_ANY_VERSION) == 0) {
String message = "Source version " + metaInfo.versionCode
- + " > installed version " + mCurrentPackage.versionCode;
+ + " > installed version " + mCurrentPackage.getLongVersionCode();
Slog.w(TAG, "Package " + pkgName + ": " + message);
Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
BackupManagerMonitor.EXTRA_LOG_RESTORE_VERSION,
@@ -522,7 +536,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
} else {
if (DEBUG) {
Slog.v(TAG, "Source version " + metaInfo.versionCode
- + " > installed version " + mCurrentPackage.versionCode
+ + " > installed version " + mCurrentPackage.getLongVersionCode()
+ " but restoreAnyVersion");
}
Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null,
@@ -543,7 +557,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
Slog.v(TAG, "Package " + pkgName
+ " restore version [" + metaInfo.versionCode
+ "] is compatible with installed version ["
- + mCurrentPackage.versionCode + "]");
+ + mCurrentPackage.getLongVersionCode() + "]");
}
// Reset per-package preconditions and fire the appropriate next state
@@ -635,7 +649,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
}
// Guts of a key/value restore operation
- void initiateOneRestore(PackageInfo app, int appVersionCode) {
+ void initiateOneRestore(PackageInfo app, long appVersionCode) {
final String packageName = app.packageName;
if (DEBUG) {
@@ -657,13 +671,17 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
File downloadFile = (staging) ? mStageName : mBackupDataName;
try {
+ IBackupTransport transport =
+ mTransportClient.connectOrThrow(
+ "PerformUnifiedRestoreTask.initiateOneRestore()");
+
// Run the transport's restore pass
stage = ParcelFileDescriptor.open(downloadFile,
ParcelFileDescriptor.MODE_READ_WRITE |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_TRUNCATE);
- if (mTransport.getRestoreData(stage) != BackupTransport.TRANSPORT_OK) {
+ if (transport.getRestoreData(stage) != BackupTransport.TRANSPORT_OK) {
// Transport-level failure, so we wind everything up and
// terminate the restore operation.
Slog.e(TAG, "Error getting restore data for " + packageName);
@@ -750,7 +768,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// None of this can run on the work looper here, so we spin asynchronous
// work like this:
//
- // StreamFeederThread: read data from mTransport.getNextFullRestoreDataChunk()
+ // StreamFeederThread: read data from transport.getNextFullRestoreDataChunk()
// write it into the pipe to the engine
// EngineThread: FullRestoreEngine thread communicating with the target app
//
@@ -844,10 +862,12 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// spin up the engine and start moving data to it
new Thread(mEngineThread, "unified-restore-engine").start();
+ String callerLogString = "PerformUnifiedRestoreTask$StreamFeederThread.run()";
try {
+ IBackupTransport transport = mTransportClient.connectOrThrow(callerLogString);
while (status == BackupTransport.TRANSPORT_OK) {
// have the transport write some of the restoring data to us
- int result = mTransport.getNextFullRestoreDataChunk(tWriteEnd);
+ int result = transport.getNextFullRestoreDataChunk(tWriteEnd);
if (result > 0) {
// The transport wrote this many bytes of restore data to the
// pipe, so pass it along to the engine.
@@ -936,7 +956,9 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// Something went wrong somewhere. Whether it was at the transport
// level is immaterial; we need to tell the transport to bail
try {
- mTransport.abortFullRestore();
+ IBackupTransport transport =
+ mTransportClient.connectOrThrow(callerLogString);
+ transport.abortFullRestore();
} catch (Exception e) {
// transport itself is dead; make sure we handle this as a
// fatal error
@@ -947,7 +969,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
// We also need to wipe the current target's data, as it's probably
// in an incoherent state.
backupManagerService.clearApplicationDataSynchronous(
- mCurrentPackage.packageName);
+ mCurrentPackage.packageName, false);
// Schedule the next state based on the nature of our failure
if (status == BackupTransport.TRANSPORT_ERROR) {
@@ -1039,8 +1061,11 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
Slog.d(TAG, "finishing restore mObserver=" + mObserver);
}
+ String callerLogString = "PerformUnifiedRestoreTask.finalizeRestore()";
try {
- mTransport.finishRestore();
+ IBackupTransport transport =
+ mTransportClient.connectOrThrow(callerLogString);
+ transport.finishRestore();
} catch (Exception e) {
Slog.e(TAG, "Error finishing restore", e);
}
@@ -1087,9 +1112,6 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
backupManagerService.writeRestoreTokens();
}
- // done; we can finally release the wakelock and be legitimately done.
- Slog.i(TAG, "Restore complete.");
-
synchronized (backupManagerService.getPendingRestores()) {
if (backupManagerService.getPendingRestores().size() > 0) {
if (DEBUG) {
@@ -1108,14 +1130,15 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
}
}
- backupManagerService.getWakelock().release();
+ Slog.i(TAG, "Restore complete.");
+ mListener.onFinished(callerLogString);
}
void keyValueAgentErrorCleanup() {
// If the agent fails restore, it might have put the app's data
// into an incoherent state. For consistency we wipe its data
// again in this case before continuing with normal teardown
- backupManagerService.clearApplicationDataSynchronous(mCurrentPackage.packageName);
+ backupManagerService.clearApplicationDataSynchronous(mCurrentPackage.packageName, false);
keyValueAgentCleanup();
}
@@ -1301,13 +1324,11 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
void sendOnRestorePackage(String name) {
if (mObserver != null) {
- if (mObserver != null) {
- try {
- mObserver.onUpdate(mCount, name);
- } catch (RemoteException e) {
- Slog.d(TAG, "Restore observer died in onUpdate");
- mObserver = null;
- }
+ try {
+ mObserver.onUpdate(mCount, name);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Restore observer died in onUpdate");
+ mObserver = null;
}
}
}
diff --git a/com/android/server/backup/restore/RestoreEngine.java b/com/android/server/backup/restore/RestoreEngine.java
index b2fdbb86..9d3ae864 100644
--- a/com/android/server/backup/restore/RestoreEngine.java
+++ b/com/android/server/backup/restore/RestoreEngine.java
@@ -30,8 +30,8 @@ public abstract class RestoreEngine {
public static final int TARGET_FAILURE = -2;
public static final int TRANSPORT_FAILURE = -3;
- private AtomicBoolean mRunning = new AtomicBoolean(false);
- private AtomicInteger mResult = new AtomicInteger(SUCCESS);
+ private final AtomicBoolean mRunning = new AtomicBoolean(false);
+ private final AtomicInteger mResult = new AtomicInteger(SUCCESS);
public boolean isRunning() {
return mRunning.get();
diff --git a/com/android/server/backup/testing/BackupTransportStub.java b/com/android/server/backup/testing/BackupTransportStub.java
deleted file mode 100644
index ec09f908..00000000
--- a/com/android/server/backup/testing/BackupTransportStub.java
+++ /dev/null
@@ -1,179 +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 com.android.server.backup.testing;
-
-import android.app.backup.RestoreDescription;
-import android.app.backup.RestoreSet;
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.os.IBinder;
-import android.os.ParcelFileDescriptor;
-import android.os.RemoteException;
-
-import com.android.internal.backup.IBackupTransport;
-
-/**
- * Stub backup transport, doing nothing and returning default values.
- */
-public class BackupTransportStub implements IBackupTransport {
-
- private final String mName;
-
- public BackupTransportStub(String name) {
- mName = name;
- }
-
- @Override
- public IBinder asBinder() {
- return null;
- }
-
- @Override
- public String name() throws RemoteException {
- return mName;
- }
-
- @Override
- public Intent configurationIntent() throws RemoteException {
- return null;
- }
-
- @Override
- public String currentDestinationString() throws RemoteException {
- return null;
- }
-
- @Override
- public Intent dataManagementIntent() throws RemoteException {
- return null;
- }
-
- @Override
- public String dataManagementLabel() throws RemoteException {
- return null;
- }
-
- @Override
- public String transportDirName() throws RemoteException {
- return null;
- }
-
- @Override
- public long requestBackupTime() throws RemoteException {
- return 0;
- }
-
- @Override
- public int initializeDevice() throws RemoteException {
- return 0;
- }
-
- @Override
- public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags)
- throws RemoteException {
- return 0;
- }
-
- @Override
- public int clearBackupData(PackageInfo packageInfo) throws RemoteException {
- return 0;
- }
-
- @Override
- public int finishBackup() throws RemoteException {
- return 0;
- }
-
- @Override
- public RestoreSet[] getAvailableRestoreSets() throws RemoteException {
- return new RestoreSet[0];
- }
-
- @Override
- public long getCurrentRestoreSet() throws RemoteException {
- return 0;
- }
-
- @Override
- public int startRestore(long token, PackageInfo[] packages) throws RemoteException {
- return 0;
- }
-
- @Override
- public RestoreDescription nextRestorePackage() throws RemoteException {
- return null;
- }
-
- @Override
- public int getRestoreData(ParcelFileDescriptor outFd) throws RemoteException {
- return 0;
- }
-
- @Override
- public void finishRestore() throws RemoteException {
-
- }
-
- @Override
- public long requestFullBackupTime() throws RemoteException {
- return 0;
- }
-
- @Override
- public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket,
- int flags)
- throws RemoteException {
- return 0;
- }
-
- @Override
- public int checkFullBackupSize(long size) throws RemoteException {
- return 0;
- }
-
- @Override
- public int sendBackupData(int numBytes) throws RemoteException {
- return 0;
- }
-
- @Override
- public void cancelFullBackup() throws RemoteException {
-
- }
-
- @Override
- public boolean isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup)
- throws RemoteException {
- return false;
- }
-
- @Override
- public long getBackupQuota(String packageName, boolean isFullBackup)
- throws RemoteException {
- return 0;
- }
-
- @Override
- public int getNextFullRestoreDataChunk(ParcelFileDescriptor socket) throws RemoteException {
- return 0;
- }
-
- @Override
- public int abortFullRestore() throws RemoteException {
- return 0;
- }
-}
diff --git a/com/android/server/backup/testing/DefaultPackageManagerWithQueryIntentServicesAsUser.java b/com/android/server/backup/testing/ShadowPackageManagerForBackup.java
index 5a0967ba..b64b59d2 100644
--- a/com/android/server/backup/testing/DefaultPackageManagerWithQueryIntentServicesAsUser.java
+++ b/com/android/server/backup/testing/ShadowPackageManagerForBackup.java
@@ -16,28 +16,22 @@
package com.android.server.backup.testing;
+import android.app.ApplicationPackageManager;
import android.content.Intent;
import android.content.pm.ResolveInfo;
-import org.robolectric.res.ResourceLoader;
-import org.robolectric.res.builder.DefaultPackageManager;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowApplicationPackageManager;
import java.util.List;
/**
* Implementation of PackageManager for Robolectric which handles queryIntentServicesAsUser().
*/
-public class DefaultPackageManagerWithQueryIntentServicesAsUser extends
- DefaultPackageManager {
-
- /* package */
- public DefaultPackageManagerWithQueryIntentServicesAsUser(
- ResourceLoader appResourceLoader) {
- super(appResourceLoader);
- }
-
+@Implements(value = ApplicationPackageManager.class, inheritImplementationMethods = true)
+public class ShadowPackageManagerForBackup extends ShadowApplicationPackageManager {
@Override
public List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int flags, int userId) {
- return super.queryIntentServices(intent, flags);
+ return queryIntentServices(intent, flags);
}
}
diff --git a/com/android/server/backup/transport/TransportClient.java b/com/android/server/backup/transport/TransportClient.java
index 65f95022..2c7a0ebd 100644
--- a/com/android/server/backup/transport/TransportClient.java
+++ b/com/android/server/backup/transport/TransportClient.java
@@ -68,6 +68,7 @@ public class TransportClient {
private final Intent mBindIntent;
private final String mIdentifier;
private final ComponentName mTransportComponent;
+ private final String mTransportDirName;
private final Handler mListenerHandler;
private final String mPrefixForLog;
private final Object mStateLock = new Object();
@@ -86,8 +87,15 @@ public class TransportClient {
Context context,
Intent bindIntent,
ComponentName transportComponent,
+ String transportDirName,
String identifier) {
- this(context, bindIntent, transportComponent, identifier, Handler.getMain());
+ this(
+ context,
+ bindIntent,
+ transportComponent,
+ transportDirName,
+ identifier,
+ new Handler(Looper.getMainLooper()));
}
@VisibleForTesting
@@ -95,10 +103,12 @@ public class TransportClient {
Context context,
Intent bindIntent,
ComponentName transportComponent,
+ String transportDirName,
String identifier,
Handler listenerHandler) {
mContext = context;
mTransportComponent = transportComponent;
+ mTransportDirName = transportDirName;
mBindIntent = bindIntent;
mIdentifier = identifier;
mListenerHandler = listenerHandler;
@@ -112,6 +122,10 @@ public class TransportClient {
return mTransportComponent;
}
+ public String getTransportDirName() {
+ return mTransportDirName;
+ }
+
// Calls to onServiceDisconnected() or onBindingDied() turn TransportClient UNUSABLE. After one
// of these calls, if a binding happen again the new service can be a different instance. Since
// transports are stateful, we don't want a new instance responding for an old instance's state.
diff --git a/com/android/server/backup/transport/TransportClientManager.java b/com/android/server/backup/transport/TransportClientManager.java
index 1cbe7471..bb550f6e 100644
--- a/com/android/server/backup/transport/TransportClientManager.java
+++ b/com/android/server/backup/transport/TransportClientManager.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.Intent;
import android.util.Log;
+import com.android.internal.backup.IBackupTransport;
import com.android.server.backup.TransportManager;
/**
@@ -47,12 +48,17 @@ public class TransportClientManager {
* transportComponent}.
*
* @param transportComponent The {@link ComponentName} of the transport.
+ * @param transportDirName The {@link String} returned by
+ * {@link IBackupTransport#transportDirName()} at registration.
* @param caller A {@link String} identifying the caller for logging/debugging purposes. Check
* {@link TransportClient#connectAsync(TransportConnectionListener, String)} for more
* details.
* @return A {@link TransportClient}.
*/
- public TransportClient getTransportClient(ComponentName transportComponent, String caller) {
+ public TransportClient getTransportClient(
+ ComponentName transportComponent,
+ String transportDirName,
+ String caller) {
Intent bindIntent =
new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(transportComponent);
synchronized (mTransportClientsLock) {
@@ -61,6 +67,7 @@ public class TransportClientManager {
mContext,
bindIntent,
transportComponent,
+ transportDirName,
Integer.toString(mTransportClientsCreated));
mTransportClientsCreated++;
TransportUtils.log(Log.DEBUG, TAG, caller, "Retrieving " + transportClient);
diff --git a/com/android/server/backup/transport/TransportClientTest.java b/com/android/server/backup/transport/TransportClientTest.java
index 54d233a9..4462d2a0 100644
--- a/com/android/server/backup/transport/TransportClientTest.java
+++ b/com/android/server/backup/transport/TransportClientTest.java
@@ -18,6 +18,8 @@ package com.android.server.backup.transport;
import static com.android.server.backup.TransportManager.SERVICE_ACTION_TRANSPORT_HOST;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -36,30 +38,34 @@ import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.TransportManager;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderClasses;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
-@RunWith(RobolectricTestRunner.class)
-@Config(manifest = Config.NONE, sdk = 23)
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(manifest = Config.NONE, sdk = 26)
+@SystemLoaderClasses({TransportManager.class, TransportClient.class})
@Presubmit
public class TransportClientTest {
private static final String PACKAGE_NAME = "some.package.name";
- private static final ComponentName TRANSPORT_COMPONENT =
- new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".transport.Transport");
@Mock private Context mContext;
@Mock private TransportConnectionListener mTransportConnectionListener;
@Mock private TransportConnectionListener mTransportConnectionListener2;
@Mock private IBackupTransport.Stub mIBackupTransport;
private TransportClient mTransportClient;
+ private ComponentName mTransportComponent;
+ private String mTransportDirName;
private Intent mBindIntent;
private ShadowLooper mShadowLooper;
@@ -69,10 +75,18 @@ public class TransportClientTest {
Looper mainLooper = Looper.getMainLooper();
mShadowLooper = shadowOf(mainLooper);
- mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(TRANSPORT_COMPONENT);
+ mTransportComponent =
+ new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".transport.Transport");
+ mTransportDirName = mTransportComponent.toString();
+ mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(mTransportComponent);
mTransportClient =
new TransportClient(
- mContext, mBindIntent, TRANSPORT_COMPONENT, "1", new Handler(mainLooper));
+ mContext,
+ mBindIntent,
+ mTransportComponent,
+ mTransportDirName,
+ "1",
+ new Handler(mainLooper));
when(mContext.bindServiceAsUser(
eq(mBindIntent),
@@ -82,7 +96,16 @@ public class TransportClientTest {
.thenReturn(true);
}
- // TODO: Testing implementation? Remove?
+ @Test
+ public void testGetTransportDirName_returnsTransportDirName() {
+ assertThat(mTransportClient.getTransportDirName()).isEqualTo(mTransportDirName);
+ }
+
+ @Test
+ public void testGetTransportComponent_returnsTransportComponent() {
+ assertThat(mTransportClient.getTransportComponent()).isEqualTo(mTransportComponent);
+ }
+
@Test
public void testConnectAsync_callsBindService() throws Exception {
mTransportClient.connectAsync(mTransportConnectionListener, "caller");
@@ -101,7 +124,7 @@ public class TransportClientTest {
// Simulate framework connecting
ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
- connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
mShadowLooper.runToEndOfTasks();
verify(mTransportConnectionListener)
@@ -116,7 +139,7 @@ public class TransportClientTest {
mTransportClient.connectAsync(mTransportConnectionListener2, "caller2");
- connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
mShadowLooper.runToEndOfTasks();
verify(mTransportConnectionListener)
@@ -129,7 +152,7 @@ public class TransportClientTest {
public void testConnectAsync_whenAlreadyConnected_callsListener() throws Exception {
mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
- connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
mTransportClient.connectAsync(mTransportConnectionListener2, "caller2");
@@ -174,8 +197,8 @@ public class TransportClientTest {
throws Exception {
mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
- connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport);
- connection.onServiceDisconnected(TRANSPORT_COMPONENT);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
+ connection.onServiceDisconnected(mTransportComponent);
mTransportClient.connectAsync(mTransportConnectionListener2, "caller1");
@@ -188,9 +211,9 @@ public class TransportClientTest {
throws Exception {
mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
- connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport);
- connection.onServiceDisconnected(TRANSPORT_COMPONENT);
- connection.onServiceConnected(TRANSPORT_COMPONENT, mIBackupTransport);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
+ connection.onServiceDisconnected(mTransportComponent);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
mTransportClient.connectAsync(mTransportConnectionListener2, "caller1");
@@ -205,7 +228,7 @@ public class TransportClientTest {
mTransportClient.connectAsync(mTransportListener, "caller");
ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
- connection.onBindingDied(TRANSPORT_COMPONENT);
+ connection.onBindingDied(mTransportComponent);
mShadowLooper.runToEndOfTasks();
verify(mTransportListener).onTransportBound(isNull(), eq(mTransportClient));
@@ -219,7 +242,7 @@ public class TransportClientTest {
mTransportClient.connectAsync(mTransportListener2, "caller2");
- connection.onBindingDied(TRANSPORT_COMPONENT);
+ connection.onBindingDied(mTransportComponent);
mShadowLooper.runToEndOfTasks();
verify(mTransportListener).onTransportBound(isNull(), eq(mTransportClient));
diff --git a/com/android/server/backup/transport/TransportNotAvailableException.java b/com/android/server/backup/transport/TransportNotAvailableException.java
index c4e5a1dd..c08eb7f4 100644
--- a/com/android/server/backup/transport/TransportNotAvailableException.java
+++ b/com/android/server/backup/transport/TransportNotAvailableException.java
@@ -16,6 +16,8 @@
package com.android.server.backup.transport;
+import android.util.AndroidException;
+
import com.android.internal.backup.IBackupTransport;
/**
@@ -25,7 +27,7 @@ import com.android.internal.backup.IBackupTransport;
*
* @see TransportClient#connectAsync(TransportConnectionListener, String)
*/
-public class TransportNotAvailableException extends Exception {
+public class TransportNotAvailableException extends AndroidException {
TransportNotAvailableException() {
super("Transport not available");
}
diff --git a/com/android/server/backup/transport/TransportNotRegisteredException.java b/com/android/server/backup/transport/TransportNotRegisteredException.java
new file mode 100644
index 00000000..26bf92cb
--- /dev/null
+++ b/com/android/server/backup/transport/TransportNotRegisteredException.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.android.server.backup.transport;
+
+import android.util.AndroidException;
+
+import com.android.server.backup.TransportManager;
+
+/**
+ * Exception thrown when the transport is not registered.
+ *
+ * @see TransportManager#getTransportDirName(String)
+ * @see TransportManager#getTransportConfigurationIntent(String)
+ * @see TransportManager#getTransportDataManagementIntent(String)
+ * @see TransportManager#getTransportDataManagementLabel(String)
+ */
+public class TransportNotRegisteredException extends AndroidException {
+ public TransportNotRegisteredException(String transportName) {
+ super("Transport " + transportName + " not registered");
+ }
+}
diff --git a/com/android/server/backup/transport/TransportUtils.java b/com/android/server/backup/transport/TransportUtils.java
index 85599b78..92bba9bf 100644
--- a/com/android/server/backup/transport/TransportUtils.java
+++ b/com/android/server/backup/transport/TransportUtils.java
@@ -31,7 +31,7 @@ public class TransportUtils {
* Throws {@link TransportNotAvailableException} if {@param transport} is null. The semantics is
* similar to a {@link DeadObjectException} coming from a dead transport binder.
*/
- public static IBackupTransport checkTransport(@Nullable IBackupTransport transport)
+ public static IBackupTransport checkTransportNotNull(@Nullable IBackupTransport transport)
throws TransportNotAvailableException {
if (transport == null) {
log(Log.ERROR, TAG, "Transport not available");
diff --git a/com/android/server/backup/utils/BackupManagerMonitorUtils.java b/com/android/server/backup/utils/BackupManagerMonitorUtils.java
index 734fa1d6..010684e8 100644
--- a/com/android/server/backup/utils/BackupManagerMonitorUtils.java
+++ b/com/android/server/backup/utils/BackupManagerMonitorUtils.java
@@ -56,6 +56,8 @@ public class BackupManagerMonitorUtils {
pkg.packageName);
bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_VERSION,
pkg.versionCode);
+ bundle.putLong(BackupManagerMonitor.EXTRA_LOG_EVENT_PACKAGE_LONG_VERSION,
+ pkg.getLongVersionCode());
}
if (extras != null) {
bundle.putAll(extras);
diff --git a/com/android/server/backup/utils/FullBackupUtils.java b/com/android/server/backup/utils/FullBackupUtils.java
index b3e20dcf..a731fc9a 100644
--- a/com/android/server/backup/utils/FullBackupUtils.java
+++ b/com/android/server/backup/utils/FullBackupUtils.java
@@ -97,7 +97,7 @@ public class FullBackupUtils {
printer.println(Integer.toString(BACKUP_MANIFEST_VERSION));
printer.println(pkg.packageName);
- printer.println(Integer.toString(pkg.versionCode));
+ printer.println(Long.toString(pkg.getLongVersionCode()));
printer.println(Integer.toString(Build.VERSION.SDK_INT));
String installerName = packageManager.getInstallerPackageName(pkg.packageName);
diff --git a/com/android/server/backup/utils/TarBackupReader.java b/com/android/server/backup/utils/TarBackupReader.java
index 2910ba2e..ff9cb569 100644
--- a/com/android/server/backup/utils/TarBackupReader.java
+++ b/com/android/server/backup/utils/TarBackupReader.java
@@ -422,7 +422,7 @@ public class TarBackupReader {
LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
null);
policy = RestorePolicy.ACCEPT;
- } else if (pkgInfo.versionCode >= info.version) {
+ } else if (pkgInfo.getLongVersionCode() >= info.version) {
Slog.i(TAG, "Sig + version match; taking data");
policy = RestorePolicy.ACCEPT;
mMonitor = BackupManagerMonitorUtils.monitorEvent(
@@ -439,7 +439,7 @@ public class TarBackupReader {
Slog.i(TAG, "Data version " + info.version
+ " is newer than installed "
+ "version "
- + pkgInfo.versionCode
+ + pkgInfo.getLongVersionCode()
+ " - requiring apk");
policy = RestorePolicy.ACCEPT_IF_APK;
} else {
diff --git a/com/android/server/broadcastradio/BroadcastRadioService.java b/com/android/server/broadcastradio/BroadcastRadioService.java
index 8fdbcf6c..30641440 100644
--- a/com/android/server/broadcastradio/BroadcastRadioService.java
+++ b/com/android/server/broadcastradio/BroadcastRadioService.java
@@ -16,6 +16,7 @@
package com.android.server.broadcastradio;
+import android.annotation.NonNull;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -28,14 +29,16 @@ import android.os.ParcelableException;
import com.android.server.SystemService;
import java.util.List;
+import java.util.Objects;
+import java.util.OptionalInt;
public class BroadcastRadioService extends SystemService {
private final ServiceImpl mServiceImpl = new ServiceImpl();
- /**
- * This field is used by native code, do not access or modify.
- */
- private final long mNativeContext = nativeInit();
+ private final com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1 =
+ new com.android.server.broadcastradio.hal1.BroadcastRadioService();
+ private final com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2 =
+ new com.android.server.broadcastradio.hal2.BroadcastRadioService();
private final Object mLock = new Object();
private List<RadioManager.ModuleProperties> mModules = null;
@@ -45,22 +48,18 @@ public class BroadcastRadioService extends SystemService {
}
@Override
- protected void finalize() throws Throwable {
- nativeFinalize(mNativeContext);
- super.finalize();
- }
-
- private native long nativeInit();
- private native void nativeFinalize(long nativeContext);
- private native List<RadioManager.ModuleProperties> nativeLoadModules(long nativeContext);
- private native Tuner nativeOpenTuner(long nativeContext, int moduleId,
- RadioManager.BandConfig config, boolean withAudio, ITunerCallback callback);
-
- @Override
public void onStart() {
publishBinderService(Context.RADIO_SERVICE, mServiceImpl);
}
+ /**
+ * Finds next available index for newly loaded modules.
+ */
+ private static int getNextId(@NonNull List<RadioManager.ModuleProperties> modules) {
+ OptionalInt max = modules.stream().mapToInt(RadioManager.ModuleProperties::getId).max();
+ return max.isPresent() ? max.getAsInt() + 1 : 0;
+ }
+
private class ServiceImpl extends IRadioService.Stub {
private void enforcePolicyAccess() {
if (PackageManager.PERMISSION_GRANTED != getContext().checkCallingPermission(
@@ -75,11 +74,8 @@ public class BroadcastRadioService extends SystemService {
synchronized (mLock) {
if (mModules != null) return mModules;
- mModules = nativeLoadModules(mNativeContext);
- if (mModules == null) {
- throw new ParcelableException(new NullPointerException(
- "couldn't load radio modules"));
- }
+ mModules = mHal1.loadModules();
+ mModules.addAll(mHal2.loadModules(getNextId(mModules)));
return mModules;
}
@@ -93,7 +89,11 @@ public class BroadcastRadioService extends SystemService {
throw new IllegalArgumentException("Callback must not be empty");
}
synchronized (mLock) {
- return nativeOpenTuner(mNativeContext, moduleId, bandConfig, withAudio, callback);
+ if (mHal2.hasModule(moduleId)) {
+ throw new RuntimeException("Not implemented");
+ } else {
+ return mHal1.openTuner(moduleId, bandConfig, withAudio, callback);
+ }
}
}
}
diff --git a/com/android/server/broadcastradio/hal1/BroadcastRadioService.java b/com/android/server/broadcastradio/hal1/BroadcastRadioService.java
new file mode 100644
index 00000000..e8ac5477
--- /dev/null
+++ b/com/android/server/broadcastradio/hal1/BroadcastRadioService.java
@@ -0,0 +1,66 @@
+/**
+ * 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 com.android.server.broadcastradio.hal1;
+
+import android.annotation.NonNull;
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.radio.IRadioService;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+import android.os.ParcelableException;
+
+import com.android.server.SystemService;
+
+import java.util.List;
+import java.util.Objects;
+
+public class BroadcastRadioService {
+ /**
+ * This field is used by native code, do not access or modify.
+ */
+ private final long mNativeContext = nativeInit();
+
+ private final Object mLock = new Object();
+
+ @Override
+ protected void finalize() throws Throwable {
+ nativeFinalize(mNativeContext);
+ super.finalize();
+ }
+
+ private native long nativeInit();
+ private native void nativeFinalize(long nativeContext);
+ private native List<RadioManager.ModuleProperties> nativeLoadModules(long nativeContext);
+ private native Tuner nativeOpenTuner(long nativeContext, int moduleId,
+ RadioManager.BandConfig config, boolean withAudio, ITunerCallback callback);
+
+ public @NonNull List<RadioManager.ModuleProperties> loadModules() {
+ synchronized (mLock) {
+ return Objects.requireNonNull(nativeLoadModules(mNativeContext));
+ }
+ }
+
+ public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig,
+ boolean withAudio, @NonNull ITunerCallback callback) {
+ synchronized (mLock) {
+ return nativeOpenTuner(mNativeContext, moduleId, bandConfig, withAudio, callback);
+ }
+ }
+}
diff --git a/com/android/server/broadcastradio/Convert.java b/com/android/server/broadcastradio/hal1/Convert.java
index 125554fc..80c77625 100644
--- a/com/android/server/broadcastradio/Convert.java
+++ b/com/android/server/broadcastradio/hal1/Convert.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.broadcastradio;
+package com.android.server.broadcastradio.hal1;
import android.annotation.NonNull;
import android.annotation.Nullable;
diff --git a/com/android/server/broadcastradio/Tuner.java b/com/android/server/broadcastradio/hal1/Tuner.java
index 2ea42718..cce534d3 100644
--- a/com/android/server/broadcastradio/Tuner.java
+++ b/com/android/server/broadcastradio/hal1/Tuner.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.broadcastradio;
+package com.android.server.broadcastradio.hal1;
import android.annotation.NonNull;
import android.graphics.Bitmap;
diff --git a/com/android/server/broadcastradio/TunerCallback.java b/com/android/server/broadcastradio/hal1/TunerCallback.java
index 2460c67a..673ff88d 100644
--- a/com/android/server/broadcastradio/TunerCallback.java
+++ b/com/android/server/broadcastradio/hal1/TunerCallback.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.broadcastradio;
+package com.android.server.broadcastradio.hal1;
import android.annotation.NonNull;
import android.hardware.radio.ITuner;
diff --git a/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
new file mode 100644
index 00000000..76294774
--- /dev/null
+++ b/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
@@ -0,0 +1,79 @@
+/**
+ * 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 com.android.server.broadcastradio.hal2;
+
+import android.annotation.NonNull;
+import android.hardware.radio.RadioManager;
+import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.hidl.manager.V1_0.IServiceManager;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class BroadcastRadioService {
+ private static final String TAG = "BcRadio2Srv";
+
+ private final Map<Integer, RadioModule> mModules = new HashMap<>();
+
+ private static @NonNull List<String> listByInterface(@NonNull String fqName) {
+ try {
+ IServiceManager manager = IServiceManager.getService();
+ if (manager == null) {
+ Slog.e(TAG, "Failed to get HIDL Service Manager");
+ return Collections.emptyList();
+ }
+
+ List<String> list = manager.listByInterface(fqName);
+ if (list == null) {
+ Slog.e(TAG, "Didn't get interface list from HIDL Service Manager");
+ return Collections.emptyList();
+ }
+ return list;
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "Failed fetching interface list", ex);
+ return Collections.emptyList();
+ }
+ }
+
+ public @NonNull Collection<RadioManager.ModuleProperties> loadModules(int idx) {
+ Slog.v(TAG, "loadModules(" + idx + ")");
+
+ for (String serviceName : listByInterface(IBroadcastRadio.kInterfaceName)) {
+ Slog.v(TAG, "checking service: " + serviceName);
+
+ RadioModule module = RadioModule.tryLoadingModule(idx, serviceName);
+ if (module != null) {
+ Slog.i(TAG, "loaded broadcast radio module " + idx + ": " +
+ serviceName + " (HAL 2.0)");
+ mModules.put(idx++, module);
+ }
+ }
+
+ return mModules.values().stream().map(module -> module.mProperties).
+ collect(Collectors.toList());
+ }
+
+ public boolean hasModule(int id) {
+ return mModules.containsKey(id);
+ }
+}
diff --git a/com/android/server/broadcastradio/hal2/Convert.java b/com/android/server/broadcastradio/hal2/Convert.java
new file mode 100644
index 00000000..c3394e9e
--- /dev/null
+++ b/com/android/server/broadcastradio/hal2/Convert.java
@@ -0,0 +1,132 @@
+/**
+ * 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 com.android.server.broadcastradio.hal2;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.broadcastradio.V2_0.Properties;
+import android.hardware.broadcastradio.V2_0.VendorKeyValue;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.util.Slog;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+class Convert {
+ private static final String TAG = "BcRadio2Srv.convert";
+
+ private static @NonNull Map<String, String>
+ vendorInfoFromHal(@Nullable List<VendorKeyValue> info) {
+ if (info == null) return Collections.emptyMap();
+
+ Map<String, String> map = new HashMap<>();
+ for (VendorKeyValue kvp : info) {
+ if (kvp.key == null || kvp.value == null) {
+ Slog.w(TAG, "VendorKeyValue contains null pointers");
+ continue;
+ }
+ map.put(kvp.key, kvp.value);
+ }
+
+ return map;
+ }
+
+ private static @NonNull int[]
+ identifierTypesToProgramTypes(@NonNull int[] idTypes) {
+ Set<Integer> pTypes = new HashSet<>();
+
+ for (int idType : idTypes) {
+ switch (idType) {
+ case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY:
+ case ProgramSelector.IDENTIFIER_TYPE_RDS_PI:
+ // TODO(b/69958423): verify AM/FM with region info
+ pTypes.add(ProgramSelector.PROGRAM_TYPE_AM);
+ pTypes.add(ProgramSelector.PROGRAM_TYPE_FM);
+ break;
+ case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT:
+ // TODO(b/69958423): verify AM/FM with region info
+ pTypes.add(ProgramSelector.PROGRAM_TYPE_AM_HD);
+ pTypes.add(ProgramSelector.PROGRAM_TYPE_FM_HD);
+ break;
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
+ pTypes.add(ProgramSelector.PROGRAM_TYPE_DAB);
+ break;
+ case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
+ case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
+ pTypes.add(ProgramSelector.PROGRAM_TYPE_DRMO);
+ break;
+ case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
+ case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
+ pTypes.add(ProgramSelector.PROGRAM_TYPE_SXM);
+ break;
+ default:
+ break;
+ }
+ if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START
+ && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) {
+ pTypes.add(idType);
+ }
+ }
+
+ return pTypes.stream().mapToInt(Integer::intValue).toArray();
+ }
+
+ static @NonNull RadioManager.ModuleProperties
+ propertiesFromHal(int id, @NonNull String serviceName, Properties prop) {
+ Objects.requireNonNull(prop);
+
+ // TODO(b/69958423): implement region info
+ RadioManager.BandDescriptor[] bands = new RadioManager.BandDescriptor[0];
+
+ int[] supportedIdentifierTypes = prop.supportedIdentifierTypes.stream().
+ mapToInt(Integer::intValue).toArray();
+ int[] supportedProgramTypes = identifierTypesToProgramTypes(supportedIdentifierTypes);
+
+ return new RadioManager.ModuleProperties(
+ id,
+ serviceName,
+
+ // There is no Class concept in HAL 2.0.
+ RadioManager.CLASS_AM_FM,
+
+ prop.maker,
+ prop.product,
+ prop.version,
+ prop.serial,
+
+ /* HAL 2.0 only supports single tuner and audio source per
+ * HAL implementation instance. */
+ 1, // numTuners
+ 1, // numAudioSources
+ false, // isCaptureSupported
+
+ bands,
+ true, // isBgScanSupported is deprecated
+ supportedProgramTypes,
+ supportedIdentifierTypes,
+ vendorInfoFromHal(prop.vendorInfo));
+ }
+}
diff --git a/com/android/server/broadcastradio/hal2/RadioModule.java b/com/android/server/broadcastradio/hal2/RadioModule.java
new file mode 100644
index 00000000..34c1b0ce
--- /dev/null
+++ b/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -0,0 +1,54 @@
+/**
+ * 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 com.android.server.broadcastradio.hal2;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.radio.RadioManager;
+import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import java.util.Objects;
+
+class RadioModule {
+ private static final String TAG = "BcRadio2Srv.module";
+
+ @NonNull private final IBroadcastRadio mService;
+ @NonNull public final RadioManager.ModuleProperties mProperties;
+
+ private RadioModule(@NonNull IBroadcastRadio service,
+ @NonNull RadioManager.ModuleProperties properties) {
+ mProperties = Objects.requireNonNull(properties);
+ mService = Objects.requireNonNull(service);
+ }
+
+ public static @Nullable RadioModule tryLoadingModule(int idx, @NonNull String fqName) {
+ try {
+ IBroadcastRadio service = IBroadcastRadio.getService();
+ if (service == null) return null;
+
+ RadioManager.ModuleProperties prop =
+ Convert.propertiesFromHal(idx, fqName, service.getProperties());
+
+ return new RadioModule(service, prop);
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "failed to load module " + fqName, ex);
+ return null;
+ }
+ }
+}
diff --git a/com/android/server/companion/CompanionDeviceManagerService.java b/com/android/server/companion/CompanionDeviceManagerService.java
index f2f01cfa..d44fe4db 100644
--- a/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/com/android/server/companion/CompanionDeviceManagerService.java
@@ -21,6 +21,7 @@ import static com.android.internal.util.CollectionUtils.size;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;
+import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;
import android.Manifest;
import android.annotation.CheckResult;
@@ -69,6 +70,7 @@ import com.android.internal.content.PackageMonitor;
import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
+import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.FgThread;
import com.android.server.SystemService;
@@ -440,32 +442,35 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
return;
}
- Binder.withCleanCallingIdentity(() -> {
- try {
- if (containsEither(packageInfo.requestedPermissions,
- Manifest.permission.RUN_IN_BACKGROUND,
- Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
- mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName);
- } else {
- mIdleController.removePowerSaveWhitelistApp(packageInfo.packageName);
- }
- } catch (RemoteException e) {
- /* ignore - local call */
- }
+ Binder.withCleanCallingIdentity(obtainRunnable(CompanionDeviceManagerService::
+ updateSpecialAccessPermissionAsSystem, this, packageInfo).recycleOnUse());
+ }
- NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
+ private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
+ try {
if (containsEither(packageInfo.requestedPermissions,
- Manifest.permission.USE_DATA_IN_BACKGROUND,
- Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
- networkPolicyManager.addUidPolicy(
- packageInfo.applicationInfo.uid,
- NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+ android.Manifest.permission.RUN_IN_BACKGROUND,
+ android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
+ mIdleController.addPowerSaveWhitelistApp(packageInfo.packageName);
} else {
- networkPolicyManager.removeUidPolicy(
- packageInfo.applicationInfo.uid,
- NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+ mIdleController.removePowerSaveWhitelistApp(packageInfo.packageName);
}
- });
+ } catch (RemoteException e) {
+ /* ignore - local call */
+ }
+
+ NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
+ if (containsEither(packageInfo.requestedPermissions,
+ android.Manifest.permission.USE_DATA_IN_BACKGROUND,
+ android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
+ networkPolicyManager.addUidPolicy(
+ packageInfo.applicationInfo.uid,
+ NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+ } else {
+ networkPolicyManager.removeUidPolicy(
+ packageInfo.applicationInfo.uid,
+ NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+ }
}
private static <T> boolean containsEither(T[] array, T a, T b) {
@@ -474,17 +479,17 @@ public class CompanionDeviceManagerService extends SystemService implements Bind
@Nullable
private PackageInfo getPackageInfo(String packageName, int userId) {
- return Binder.withCleanCallingIdentity(() -> {
+ return Binder.withCleanCallingIdentity(PooledLambda.obtainSupplier((context, pkg, id) -> {
try {
- return getContext().getPackageManager().getPackageInfoAsUser(
- packageName,
+ return context.getPackageManager().getPackageInfoAsUser(
+ pkg,
PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS,
- userId);
+ id);
} catch (PackageManager.NameNotFoundException e) {
- Slog.e(LOG_TAG, "Failed to get PackageInfo for package " + packageName, e);
+ Slog.e(LOG_TAG, "Failed to get PackageInfo for package " + pkg, e);
return null;
}
- });
+ }, getContext(), packageName, userId).recycleOnUse());
}
private void recordAssociation(String priviledgedPackage, String deviceAddress) {
diff --git a/com/android/server/connectivity/DefaultNetworkMetrics.java b/com/android/server/connectivity/DefaultNetworkMetrics.java
index 28c35858..bd2e96ed 100644
--- a/com/android/server/connectivity/DefaultNetworkMetrics.java
+++ b/com/android/server/connectivity/DefaultNetworkMetrics.java
@@ -51,6 +51,7 @@ public class DefaultNetworkMetrics {
// Information about the current status of the default network.
@GuardedBy("this")
private DefaultNetworkEvent mCurrentDefaultNetwork;
+ // True if the current default network has been validated.
@GuardedBy("this")
private boolean mIsCurrentlyValid;
@GuardedBy("this")
@@ -71,6 +72,8 @@ public class DefaultNetworkMetrics {
printEvent(localTimeMs, pw, ev);
}
mCurrentDefaultNetwork.updateDuration(timeMs);
+ // When printing default network events for bug reports, update validation time
+ // and refresh the last validation timestmap for future validation time updates.
if (mIsCurrentlyValid) {
updateValidationTime(timeMs);
mLastValidationTimeMs = timeMs;
@@ -92,11 +95,13 @@ public class DefaultNetworkMetrics {
}
public synchronized void logDefaultNetworkValidity(long timeMs, boolean isValid) {
+ // Transition from valid to invalid: update validity duration since last update
if (!isValid && mIsCurrentlyValid) {
mIsCurrentlyValid = false;
updateValidationTime(timeMs);
}
+ // Transition from invalid to valid: simply mark the validation timestamp.
if (isValid && !mIsCurrentlyValid) {
mIsCurrentlyValid = true;
mLastValidationTimeMs = timeMs;
@@ -114,6 +119,9 @@ public class DefaultNetworkMetrics {
}
private void logCurrentDefaultNetwork(long timeMs, NetworkAgentInfo oldNai) {
+ if (mIsCurrentlyValid) {
+ updateValidationTime(timeMs);
+ }
DefaultNetworkEvent ev = mCurrentDefaultNetwork;
ev.updateDuration(timeMs);
ev.previousTransports = mLastTransports;
@@ -122,7 +130,6 @@ public class DefaultNetworkMetrics {
// The system acquired a new default network.
fillLinkInfo(ev, oldNai);
ev.finalScore = oldNai.getCurrentScore();
- ev.validatedMs = ev.durationMs;
}
// Only change transport of the previous default network if the event currently logged
// corresponds to an existing default network, and not to the absence of a default network.
@@ -143,9 +150,10 @@ public class DefaultNetworkMetrics {
fillLinkInfo(ev, newNai);
ev.initialScore = newNai.getCurrentScore();
if (newNai.lastValidated) {
- mIsCurrentlyValid = true;
- mLastValidationTimeMs = timeMs;
+ logDefaultNetworkValidity(timeMs, true);
}
+ } else {
+ mIsCurrentlyValid = false;
}
mCurrentDefaultNetwork = ev;
}
diff --git a/com/android/server/connectivity/NetdEventListenerService.java b/com/android/server/connectivity/NetdEventListenerService.java
index e243e56c..979beed5 100644
--- a/com/android/server/connectivity/NetdEventListenerService.java
+++ b/com/android/server/connectivity/NetdEventListenerService.java
@@ -244,7 +244,7 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
event.timestampMs = timestampMs;
event.uid = uid;
event.ethertype = ethertype;
- event.dstHwAddr = new MacAddress(dstHw);
+ event.dstHwAddr = MacAddress.fromBytes(dstHw);
event.srcIp = srcIp;
event.dstIp = dstIp;
event.ipNextHeader = ipNextHeader;
diff --git a/com/android/server/content/ContentService.java b/com/android/server/content/ContentService.java
index c4e6ff6b..6280edb8 100644
--- a/com/android/server/content/ContentService.java
+++ b/com/android/server/content/ContentService.java
@@ -644,6 +644,11 @@ public final class ContentService extends IContentService.Stub {
int userId = UserHandle.getCallingUserId();
final int callingUid = Binder.getCallingUid();
+ if (request.isPeriodic()) {
+ mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
+ "no permission to write the sync settings");
+ }
+
long identityToken = clearCallingIdentity();
try {
SyncStorageEngine.EndPoint info;
@@ -653,8 +658,6 @@ public final class ContentService extends IContentService.Stub {
info = new SyncStorageEngine.EndPoint(account, provider, userId);
if (request.isPeriodic()) {
// Remove periodic sync.
- mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS,
- "no permission to write the sync settings");
getSyncManager().removePeriodicSync(info, extras,
"cancelRequest() by uid=" + callingUid);
}
diff --git a/com/android/server/content/SyncJobService.java b/com/android/server/content/SyncJobService.java
index 07f04b1b..29b322ea 100644
--- a/com/android/server/content/SyncJobService.java
+++ b/com/android/server/content/SyncJobService.java
@@ -120,6 +120,7 @@ public class SyncJobService extends JobService {
synchronized (jobParamsMap) {
JobParameters params = jobParamsMap.get(jobId);
mLogger.log("callJobFinished()",
+ " jobid=", jobId,
" needsReschedule=", needsReschedule,
" ", mLogger.jobParametersToString(params),
" why=", why);
diff --git a/com/android/server/content/SyncManager.java b/com/android/server/content/SyncManager.java
index 9cd52d77..965159bd 100644
--- a/com/android/server/content/SyncManager.java
+++ b/com/android/server/content/SyncManager.java
@@ -323,7 +323,7 @@ public class SyncManager {
private final BroadcastReceiver mAccountsUpdatedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- EndPoint target = new EndPoint(null, null, context.getUserId());
+ EndPoint target = new EndPoint(null, null, getSendingUserId());
updateRunningAccounts(target /* sync targets for user */);
}
};
@@ -565,7 +565,7 @@ public class SyncManager {
mLogger = SyncLogger.getInstance();
- SyncStorageEngine.init(context);
+ SyncStorageEngine.init(context, BackgroundThread.get().getLooper());
mSyncStorageEngine = SyncStorageEngine.getSingleton();
mSyncStorageEngine.setOnSyncRequestListener(new OnSyncRequestListener() {
@Override
@@ -735,15 +735,15 @@ public class SyncManager {
}
public void onStartUser(int userHandle) {
- mLogger.log("onStartUser: user=", userHandle);
+ mSyncHandler.post(() -> mLogger.log("onStartUser: user=", userHandle));
}
public void onUnlockUser(int userHandle) {
- mLogger.log("onUnlockUser: user=", userHandle);
+ mSyncHandler.post(() -> mLogger.log("onUnlockUser: user=", userHandle));
}
public void onStopUser(int userHandle) {
- mLogger.log("onStopUser: user=", userHandle);
+ mSyncHandler.post(() -> mLogger.log("onStopUser: user=", userHandle));
}
diff --git a/com/android/server/content/SyncStorageEngine.java b/com/android/server/content/SyncStorageEngine.java
index 3591871f..e4986660 100644
--- a/com/android/server/content/SyncStorageEngine.java
+++ b/com/android/server/content/SyncStorageEngine.java
@@ -36,6 +36,7 @@ import android.database.sqlite.SQLiteQueryBuilder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
+import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
import android.os.RemoteCallbackList;
@@ -69,7 +70,7 @@ import java.util.TimeZone;
*
* @hide
*/
-public class SyncStorageEngine extends Handler {
+public class SyncStorageEngine {
private static final String TAG = "SyncManager";
private static final String TAG_FILE = "SyncManagerFile";
@@ -462,7 +463,10 @@ public class SyncStorageEngine extends Handler {
private boolean mGrantSyncAdaptersAccountAccess;
- private SyncStorageEngine(Context context, File dataDir) {
+ private final MyHandler mHandler;
+
+ private SyncStorageEngine(Context context, File dataDir, Looper looper) {
+ mHandler = new MyHandler(looper);
mContext = context;
sSyncStorageEngine = this;
@@ -491,15 +495,15 @@ public class SyncStorageEngine extends Handler {
}
public static SyncStorageEngine newTestInstance(Context context) {
- return new SyncStorageEngine(context, context.getFilesDir());
+ return new SyncStorageEngine(context, context.getFilesDir(), Looper.getMainLooper());
}
- public static void init(Context context) {
+ public static void init(Context context, Looper looper) {
if (sSyncStorageEngine != null) {
return;
}
File dataDir = Environment.getDataDirectory();
- sSyncStorageEngine = new SyncStorageEngine(context, dataDir);
+ sSyncStorageEngine = new SyncStorageEngine(context, dataDir, looper);
}
public static SyncStorageEngine getSingleton() {
@@ -527,14 +531,21 @@ public class SyncStorageEngine extends Handler {
}
}
- @Override public void handleMessage(Message msg) {
- if (msg.what == MSG_WRITE_STATUS) {
- synchronized (mAuthorities) {
- writeStatusLocked();
- }
- } else if (msg.what == MSG_WRITE_STATISTICS) {
- synchronized (mAuthorities) {
- writeStatisticsLocked();
+ private class MyHandler extends Handler {
+ public MyHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_WRITE_STATUS) {
+ synchronized (mAuthorities) {
+ writeStatusLocked();
+ }
+ } else if (msg.what == MSG_WRITE_STATISTICS) {
+ synchronized (mAuthorities) {
+ writeStatisticsLocked();
+ }
}
}
}
@@ -1202,14 +1213,14 @@ public class SyncStorageEngine extends Handler {
if (writeStatusNow) {
writeStatusLocked();
- } else if (!hasMessages(MSG_WRITE_STATUS)) {
- sendMessageDelayed(obtainMessage(MSG_WRITE_STATUS),
+ } else if (!mHandler.hasMessages(MSG_WRITE_STATUS)) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_WRITE_STATUS),
WRITE_STATUS_DELAY);
}
if (writeStatisticsNow) {
writeStatisticsLocked();
- } else if (!hasMessages(MSG_WRITE_STATISTICS)) {
- sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS),
+ } else if (!mHandler.hasMessages(MSG_WRITE_STATISTICS)) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_WRITE_STATISTICS),
WRITE_STATISTICS_DELAY);
}
}
@@ -2102,7 +2113,7 @@ public class SyncStorageEngine extends Handler {
// The file is being written, so we don't need to have a scheduled
// write until the next change.
- removeMessages(MSG_WRITE_STATUS);
+ mHandler.removeMessages(MSG_WRITE_STATUS);
FileOutputStream fos = null;
try {
@@ -2210,7 +2221,7 @@ public class SyncStorageEngine extends Handler {
// The file is being written, so we don't need to have a scheduled
// write until the next change.
- removeMessages(MSG_WRITE_STATISTICS);
+ mHandler.removeMessages(MSG_WRITE_STATISTICS);
FileOutputStream fos = null;
try {
diff --git a/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
new file mode 100644
index 00000000..e55d4ea3
--- /dev/null
+++ b/com/android/server/devicepolicy/BaseIDevicePolicyManager.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 com.android.server.devicepolicy;
+
+import android.annotation.UserIdInt;
+import android.app.admin.IDevicePolicyManager;
+import android.content.ComponentName;
+import android.os.PersistableBundle;
+import android.security.keymaster.KeymasterCertificateChain;
+import android.security.keystore.ParcelableKeyGenParameterSpec;
+
+import com.android.internal.R;
+import com.android.server.SystemService;
+
+import java.util.List;
+
+/**
+ * Defines the required interface for IDevicePolicyManager implemenation.
+ *
+ * <p>The interface consists of public parts determined by {@link IDevicePolicyManager} and also
+ * several package private methods required by internal infrastructure.
+ *
+ * <p>Whenever adding an AIDL method to {@link IDevicePolicyManager}, an empty override method
+ * should be added here to avoid build breakage in downstream branches.
+ */
+abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub {
+ /**
+ * To be called by {@link DevicePolicyManagerService#Lifecycle} during the various boot phases.
+ *
+ * @see {@link SystemService#onBootPhase}.
+ */
+ abstract void systemReady(int phase);
+ /**
+ * To be called by {@link DevicePolicyManagerService#Lifecycle} when a new user starts.
+ *
+ * @see {@link SystemService#onStartUser}
+ */
+ abstract void handleStartUser(int userId);
+ /**
+ * To be called by {@link DevicePolicyManagerService#Lifecycle} when a user is being unlocked.
+ *
+ * @see {@link SystemService#onUnlockUser}
+ */
+ abstract void handleUnlockUser(int userId);
+ /**
+ * To be called by {@link DevicePolicyManagerService#Lifecycle} when a user is being stopped.
+ *
+ * @see {@link SystemService#onStopUser}
+ */
+ abstract void handleStopUser(int userId);
+
+ public void setSystemSetting(ComponentName who, String setting, String value){}
+
+ public void transferOwner(ComponentName admin, ComponentName target, PersistableBundle bundle) {}
+
+ public boolean generateKeyPair(ComponentName who, String callerPackage, String algorithm,
+ ParcelableKeyGenParameterSpec keySpec, KeymasterCertificateChain attestationChain) {
+ return false;
+ }
+
+ @Override
+ public boolean setPasswordBlacklist(ComponentName who, String name, List<String> blacklist,
+ boolean parent) {
+ return false;
+ }
+
+ @Override
+ public String getPasswordBlacklistName(ComponentName who, @UserIdInt int userId,
+ boolean parent) {
+ return null;
+ }
+
+ @Override
+ public boolean isPasswordBlacklisted(@UserIdInt int userId, String password) {
+ return false;
+ }
+
+ public boolean isUsingUnifiedPassword(ComponentName who) {
+ return true;
+ }
+
+ public boolean setKeyPairCertificate(ComponentName who, String callerPackage, String alias,
+ byte[] cert, byte[] chain, boolean isUserSelectable) {
+ return false;
+ }
+}
diff --git a/com/android/server/devicepolicy/DevicePolicyManagerService.java b/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 60c36d1b..e5351b48 100644
--- a/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -19,6 +19,8 @@ package com.android.server.devicepolicy;
import static android.Manifest.permission.BIND_DEVICE_ADMIN;
import static android.Manifest.permission.MANAGE_CA_CERTIFICATES;
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
+import static android.app.ActivityManager.USER_OP_SUCCESS;
+import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_USER;
import static android.app.admin.DevicePolicyManager.CODE_ACCOUNTS_NOT_EMPTY;
import static android.app.admin.DevicePolicyManager.CODE_ADD_MANAGED_PROFILE_DISALLOWED;
import static android.app.admin.DevicePolicyManager.CODE_CANNOT_ADD_MANAGED_PROFILE;
@@ -39,10 +41,13 @@ import static android.app.admin.DevicePolicyManager.DELEGATION_APP_RESTRICTIONS;
import static android.app.admin.DevicePolicyManager.DELEGATION_BLOCK_UNINSTALL;
import static android.app.admin.DevicePolicyManager.DELEGATION_CERT_INSTALL;
import static android.app.admin.DevicePolicyManager.DELEGATION_ENABLE_SYSTEM_APP;
+import static android.app.admin.DevicePolicyManager.DELEGATION_INSTALL_EXISTING_PACKAGE;
import static android.app.admin.DevicePolicyManager.DELEGATION_KEEP_UNINSTALLED_PACKAGES;
import static android.app.admin.DevicePolicyManager.DELEGATION_PACKAGE_ACCESS;
import static android.app.admin.DevicePolicyManager.DELEGATION_PERMISSION_GRANT;
+import static android.app.admin.DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
import static android.app.admin.DevicePolicyManager.START_USER_IN_BACKGROUND;
import static android.app.admin.DevicePolicyManager.WIPE_EUICC;
@@ -78,7 +83,6 @@ import android.app.admin.DeviceAdminInfo;
import android.app.admin.DeviceAdminReceiver;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManagerInternal;
-import android.app.admin.IDevicePolicyManager;
import android.app.admin.NetworkEvent;
import android.app.admin.PasswordMetrics;
import android.app.admin.SecurityLog;
@@ -146,10 +150,16 @@ import android.provider.ContactsContract.QuickContact;
import android.provider.ContactsInternal;
import android.provider.Settings;
import android.provider.Settings.Global;
+import android.security.Credentials;
import android.security.IKeyChainAliasCallback;
import android.security.IKeyChainService;
import android.security.KeyChain;
import android.security.KeyChain.KeyChainConnection;
+import android.security.keymaster.KeymasterCertificateChain;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.ParcelableKeyGenParameterSpec;
+import android.security.KeyStore;
+import android.security.keystore.AttestationUtils;
import android.service.persistentdata.PersistentDataBlockManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -198,6 +208,8 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
+import java.lang.IllegalStateException;
+import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.util.ArrayList;
@@ -210,11 +222,12 @@ import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
/**
* Implementation of the device policy APIs.
*/
-public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
+public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
protected static final String LOG_TAG = "DevicePolicyManager";
@@ -283,6 +296,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
private static final String ATTR_APPLICATION_RESTRICTIONS_MANAGER
= "application-restrictions-manager";
+ private static final String MANAGED_PROVISIONING_PKG = "com.android.managedprovisioning";
+
// Comprehensive list of delegations.
private static final String DELEGATIONS[] = {
DELEGATION_CERT_INSTALL,
@@ -291,7 +306,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
DELEGATION_ENABLE_SYSTEM_APP,
DELEGATION_KEEP_UNINSTALLED_PACKAGES,
DELEGATION_PACKAGE_ACCESS,
- DELEGATION_PERMISSION_GRANT
+ DELEGATION_PERMISSION_GRANT,
+ DELEGATION_INSTALL_EXISTING_PACKAGE,
+ DELEGATION_KEEP_UNINSTALLED_PACKAGES
};
/**
@@ -313,6 +330,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
private static final Set<String> SECURE_SETTINGS_DEVICEOWNER_WHITELIST;
private static final Set<String> GLOBAL_SETTINGS_WHITELIST;
private static final Set<String> GLOBAL_SETTINGS_DEPRECATED;
+ private static final Set<String> SYSTEM_SETTINGS_WHITELIST;
static {
SECURE_SETTINGS_WHITELIST = new ArraySet<>();
SECURE_SETTINGS_WHITELIST.add(Settings.Secure.DEFAULT_INPUT_METHOD);
@@ -339,6 +357,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
GLOBAL_SETTINGS_DEPRECATED.add(Settings.Global.MODE_RINGER);
GLOBAL_SETTINGS_DEPRECATED.add(Settings.Global.NETWORK_PREFERENCE);
GLOBAL_SETTINGS_DEPRECATED.add(Settings.Global.WIFI_ON);
+
+ SYSTEM_SETTINGS_WHITELIST = new ArraySet<>();
+ SYSTEM_SETTINGS_WHITELIST.add(Settings.System.SCREEN_BRIGHTNESS);
+ SYSTEM_SETTINGS_WHITELIST.add(Settings.System.SCREEN_BRIGHTNESS_MODE);
+ SYSTEM_SETTINGS_WHITELIST.add(Settings.System.SCREEN_OFF_TIMEOUT);
}
/**
@@ -376,6 +399,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
private final LockPatternUtils mLockPatternUtils;
private final DevicePolicyConstants mConstants;
private final DeviceAdminServiceController mDeviceAdminServiceController;
+ private final OverlayPackagesProvider mOverlayPackagesProvider;
/**
* Contains (package-user) pairs to remove. An entry (p, u) implies that removal of package p
@@ -451,11 +475,24 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
};
public static final class Lifecycle extends SystemService {
- private DevicePolicyManagerService mService;
+ private BaseIDevicePolicyManager mService;
public Lifecycle(Context context) {
super(context);
- mService = new DevicePolicyManagerService(context);
+ String dpmsClassName = context.getResources()
+ .getString(R.string.config_deviceSpecificDevicePolicyManagerService);
+ if (TextUtils.isEmpty(dpmsClassName)) {
+ dpmsClassName = DevicePolicyManagerService.class.getName();
+ }
+ try {
+ Class serviceClass = Class.forName(dpmsClassName);
+ Constructor constructor = serviceClass.getConstructor(Context.class);
+ mService = (BaseIDevicePolicyManager) constructor.newInstance(context);
+ } catch (Exception e) {
+ throw new IllegalStateException(
+ "Failed to instantiate DevicePolicyManagerService with class name: "
+ + dpmsClassName, e);
+ }
}
@Override
@@ -697,6 +734,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
private static final String TAG_PASSWORD_HISTORY_LENGTH = "password-history-length";
private static final String TAG_MIN_PASSWORD_LENGTH = "min-password-length";
private static final String ATTR_VALUE = "value";
+ private static final String TAG_PASSWORD_BLACKLIST = "password-blacklist";
private static final String TAG_PASSWORD_QUALITY = "password-quality";
private static final String TAG_POLICIES = "policies";
private static final String TAG_CROSS_PROFILE_WIDGET_PROVIDERS =
@@ -715,8 +753,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
private static final String TAG_ORGANIZATION_NAME = "organization-name";
private static final String ATTR_LAST_NETWORK_LOGGING_NOTIFICATION = "last-notification";
private static final String ATTR_NUM_NETWORK_LOGGING_NOTIFICATIONS = "num-notifications";
+ private static final String TAG_IS_LOGOUT_ENABLED = "is_logout_enabled";
- final DeviceAdminInfo info;
+ DeviceAdminInfo info;
static final int DEF_PASSWORD_HISTORY_LENGTH = 0;
@@ -731,7 +770,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
static final int DEF_MINIMUM_PASSWORD_NON_LETTER = 0;
@NonNull
PasswordMetrics minimumPasswordMetrics = new PasswordMetrics(
- DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, DEF_MINIMUM_PASSWORD_LENGTH,
+ PASSWORD_QUALITY_UNSPECIFIED, DEF_MINIMUM_PASSWORD_LENGTH,
DEF_MINIMUM_PASSWORD_LETTERS, DEF_MINIMUM_PASSWORD_UPPER_CASE,
DEF_MINIMUM_PASSWORD_LOWER_CASE, DEF_MINIMUM_PASSWORD_NUMERIC,
DEF_MINIMUM_PASSWORD_SYMBOLS, DEF_MINIMUM_PASSWORD_NON_LETTER);
@@ -764,6 +803,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
boolean requireAutoTime = false; // Can only be set by a device owner.
boolean forceEphemeralUsers = false; // Can only be set by a device owner.
boolean isNetworkLoggingEnabled = false; // Can only be set by a device owner.
+ boolean isLogoutEnabled = false; // Can only be set by a device owner.
// one notification after enabling + one more after reboots
static final int DEF_MAXIMUM_NETWORK_LOGGING_NOTIFICATIONS_SHOWN = 2;
@@ -827,6 +867,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
// Default title of confirm credentials screen
String organizationName = null;
+ // The blacklist data is stored in a file whose name is stored in the XML
+ String passwordBlacklistFile = null;
+
ActiveAdmin(DeviceAdminInfo _info, boolean parent) {
info = _info;
isParent = parent;
@@ -856,8 +899,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
out.startTag(null, TAG_POLICIES);
info.writePoliciesToXml(out);
out.endTag(null, TAG_POLICIES);
- if (minimumPasswordMetrics.quality
- != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
+ if (minimumPasswordMetrics.quality != PASSWORD_QUALITY_UNSPECIFIED) {
out.startTag(null, TAG_PASSWORD_QUALITY);
out.attribute(null, ATTR_VALUE, Integer.toString(minimumPasswordMetrics.quality));
out.endTag(null, TAG_PASSWORD_QUALITY);
@@ -909,6 +951,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
out.endTag(null, TAG_MIN_PASSWORD_NONLETTER);
}
}
+ if (passwordBlacklistFile != null) {
+ out.startTag(null, TAG_PASSWORD_BLACKLIST);
+ out.attribute(null, ATTR_VALUE, passwordBlacklistFile);
+ out.endTag(null, TAG_PASSWORD_BLACKLIST);
+ }
if (maximumTimeToUnlock != DEF_MAXIMUM_TIME_TO_UNLOCK) {
out.startTag(null, TAG_MAX_TIME_TO_UNLOCK);
out.attribute(null, ATTR_VALUE, Long.toString(maximumTimeToUnlock));
@@ -1081,6 +1128,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
out.text(organizationName);
out.endTag(null, TAG_ORGANIZATION_NAME);
}
+ if (isLogoutEnabled) {
+ out.startTag(null, TAG_IS_LOGOUT_ENABLED);
+ out.attribute(null, ATTR_VALUE, Boolean.toString(isLogoutEnabled));
+ out.endTag(null, TAG_IS_LOGOUT_ENABLED);
+ }
}
void writePackageListToXml(XmlSerializer out, String outerTag,
@@ -1143,7 +1195,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
} else if (TAG_MIN_PASSWORD_NONLETTER.equals(tag)) {
minimumPasswordMetrics.nonLetter = Integer.parseInt(
parser.getAttributeValue(null, ATTR_VALUE));
- } else if (TAG_MAX_TIME_TO_UNLOCK.equals(tag)) {
+ } else if (TAG_PASSWORD_BLACKLIST.equals(tag)) {
+ passwordBlacklistFile = parser.getAttributeValue(null, ATTR_VALUE);
+ }else if (TAG_MAX_TIME_TO_UNLOCK.equals(tag)) {
maximumTimeToUnlock = Long.parseLong(
parser.getAttributeValue(null, ATTR_VALUE));
} else if (TAG_STRONG_AUTH_UNLOCK_TIMEOUT.equals(tag)) {
@@ -1254,6 +1308,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
} else {
Log.w(LOG_TAG, "Missing text when loading organization name");
}
+ } else if (TAG_IS_LOGOUT_ENABLED.equals(tag)) {
+ isLogoutEnabled = Boolean.parseBoolean(
+ parser.getAttributeValue(null, ATTR_VALUE));
} else {
Slog.w(LOG_TAG, "Unknown admin tag: " + tag);
XmlUtils.skipCurrentTag(parser);
@@ -1359,6 +1416,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
return userRestrictions;
}
+ public void transfer(DeviceAdminInfo deviceAdminInfo) {
+ if (hasParentActiveAdmin()) {
+ parentAdmin.info = deviceAdminInfo;
+ }
+ info = deviceAdminInfo;
+ }
+
void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.print("uid="); pw.println(getUid());
pw.print(prefix); pw.print("testOnlyAdmin=");
@@ -1388,6 +1452,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
pw.println(minimumPasswordMetrics.symbols);
pw.print(prefix); pw.print("minimumPasswordNonLetter=");
pw.println(minimumPasswordMetrics.nonLetter);
+ pw.print(prefix); pw.print("passwordBlacklist=");
+ pw.println(passwordBlacklistFile != null);
pw.print(prefix); pw.print("maximumTimeToUnlock=");
pw.println(maximumTimeToUnlock);
pw.print(prefix); pw.print("strongAuthUnlockTimeout=");
@@ -1551,6 +1617,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
mContext = context;
}
+ public boolean hasFeature() {
+ return getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN);
+ }
+
Context createContextAsUser(UserHandle user) throws PackageManager.NameNotFoundException {
final String packageName = mContext.getPackageName();
return mContext.createPackageContextAsUser(packageName, 0, user);
@@ -1636,6 +1706,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
return new LockPatternUtils(mContext);
}
+ PasswordBlacklist newPasswordBlacklist(File file) {
+ return new PasswordBlacklist(file);
+ }
+
boolean storageManagerIsFileBasedEncryptionEnabled() {
return StorageManager.isFileEncryptedNativeOnly();
}
@@ -1803,6 +1877,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
Settings.Global.putString(mContext.getContentResolver(), name, value);
}
+ void settingsSystemPutString(String name, String value) {
+ Settings.System.putString(mContext.getContentResolver(), name, value);
+ }
+
void securityLogSetLoggingEnabledProperty(boolean enabled) {
SecurityLog.setLoggingEnabledProperty(enabled);
}
@@ -1848,8 +1926,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
// TODO: why does SecurityLogMonitor need to be created even when mHasFeature == false?
mSecurityLogMonitor = new SecurityLogMonitor(this);
- mHasFeature = mInjector.getPackageManager()
- .hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN);
+ mHasFeature = mInjector.hasFeature();
mIsWatch = mInjector.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_WATCH);
mBackgroundHandler = BackgroundThread.getHandler();
@@ -1859,6 +1936,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
mDeviceAdminServiceController = new DeviceAdminServiceController(this, mConstants);
+ mOverlayPackagesProvider = new OverlayPackagesProvider(mContext);
+
if (!mHasFeature) {
// Skip the rest of the initialization
return;
@@ -2489,7 +2568,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
public DeviceAdminInfo findAdmin(ComponentName adminName, int userHandle,
- boolean throwForMissiongPermission) {
+ boolean throwForMissingPermission) {
if (!mHasFeature) {
return null;
}
@@ -2512,7 +2591,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
final String message = "DeviceAdminReceiver " + adminName + " must be protected with "
+ permission.BIND_DEVICE_ADMIN;
Slog.w(LOG_TAG, message);
- if (throwForMissiongPermission &&
+ if (throwForMissingPermission &&
ai.applicationInfo.targetSdkVersion > Build.VERSION_CODES.M) {
throw new IllegalArgumentException(message);
}
@@ -2527,11 +2606,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
}
- private JournaledFile makeJournaledFile(int userHandle) {
- final String base = userHandle == UserHandle.USER_SYSTEM
- ? mInjector.getDevicePolicyFilePathForSystemUser() + DEVICE_POLICIES_XML
- : new File(mInjector.environmentGetUserSystemDirectory(userHandle),
- DEVICE_POLICIES_XML).getAbsolutePath();
+ private File getPolicyFileDirectory(@UserIdInt int userId) {
+ return userId == UserHandle.USER_SYSTEM
+ ? new File(mInjector.getDevicePolicyFilePathForSystemUser())
+ : mInjector.environmentGetUserSystemDirectory(userId);
+ }
+
+ private JournaledFile makeJournaledFile(@UserIdInt int userId) {
+ final String base = new File(getPolicyFileDirectory(userId), DEVICE_POLICIES_XML)
+ .getAbsolutePath();
if (VERBOSE_LOG) {
Log.v(LOG_TAG, "Opening " + base);
}
@@ -2837,7 +2920,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
try {
DeviceAdminInfo dai = findAdmin(
ComponentName.unflattenFromString(name), userHandle,
- /* throwForMissionPermission= */ false);
+ /* throwForMissingPermission= */ false);
if (VERBOSE_LOG
&& (UserHandle.getUserId(dai.getActivityInfo().applicationInfo.uid)
!= userHandle)) {
@@ -3033,6 +3116,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
@VisibleForTesting
+ @Override
void systemReady(int phase) {
if (!mHasFeature) {
return;
@@ -3099,6 +3183,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
}
+ @Override
void handleStartUser(int userId) {
updateScreenCaptureDisabledInWindowManager(userId,
getScreenCaptureDisabled(null, userId));
@@ -3107,10 +3192,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
startOwnerService(userId, "start-user");
}
+ @Override
void handleUnlockUser(int userId) {
startOwnerService(userId, "unlock-user");
}
+ @Override
void handleStopUser(int userId) {
stopOwnerService(userId, "stop-user");
}
@@ -3237,19 +3324,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
DevicePolicyData policy = getUserData(userHandle);
DeviceAdminInfo info = findAdmin(adminReceiver, userHandle,
- /* throwForMissionPermission= */ true);
- if (info == null) {
- throw new IllegalArgumentException("Bad admin: " + adminReceiver);
- }
- if (!info.getActivityInfo().applicationInfo.isInternal()) {
- throw new IllegalArgumentException("Only apps in internal storage can be active admin: "
- + adminReceiver);
- }
- if (info.getActivityInfo().applicationInfo.isInstantApp()) {
- throw new IllegalArgumentException("Instant apps cannot be device admins: "
- + adminReceiver);
- }
+ /* throwForMissingPermission= */ true);
synchronized (this) {
+ checkActiveAdminPrecondition(adminReceiver, info, policy);
long ident = mInjector.binderClearCallingIdentity();
try {
final ActiveAdmin existingAdmin
@@ -3257,10 +3334,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
if (!refreshing && existingAdmin != null) {
throw new IllegalArgumentException("Admin is already added");
}
- if (policy.mRemovingAdmins.contains(adminReceiver)) {
- throw new IllegalArgumentException(
- "Trying to set an admin which is being removed");
- }
ActiveAdmin newAdmin = new ActiveAdmin(info, /* parent */ false);
newAdmin.testOnlyAdmin =
(existingAdmin != null) ? existingAdmin.testOnlyAdmin
@@ -3290,6 +3363,46 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
}
+ private void transferActiveAdminUncheckedLocked(ComponentName incomingReceiver,
+ ComponentName outgoingReceiver, int userHandle) {
+ final DevicePolicyData policy = getUserData(userHandle);
+ final DeviceAdminInfo incomingDeviceInfo = findAdmin(incomingReceiver, userHandle,
+ /* throwForMissingPermission= */ true);
+ final ActiveAdmin adminToTransfer = policy.mAdminMap.get(outgoingReceiver);
+ final int oldAdminUid = adminToTransfer.getUid();
+
+ adminToTransfer.transfer(incomingDeviceInfo);
+ policy.mAdminMap.remove(outgoingReceiver);
+ policy.mAdminMap.put(incomingReceiver, adminToTransfer);
+ if (policy.mPasswordOwner == oldAdminUid) {
+ policy.mPasswordOwner = adminToTransfer.getUid();
+ }
+
+ saveSettingsLocked(userHandle);
+ //TODO: Make sure we revert back when we detect a failure.
+ sendAdminCommandLocked(adminToTransfer, DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED,
+ null, null);
+ }
+
+ private void checkActiveAdminPrecondition(ComponentName adminReceiver, DeviceAdminInfo info,
+ DevicePolicyData policy) {
+ if (info == null) {
+ throw new IllegalArgumentException("Bad admin: " + adminReceiver);
+ }
+ if (!info.getActivityInfo().applicationInfo.isInternal()) {
+ throw new IllegalArgumentException("Only apps in internal storage can be active admin: "
+ + adminReceiver);
+ }
+ if (info.getActivityInfo().applicationInfo.isInstantApp()) {
+ throw new IllegalArgumentException("Instant apps cannot be device admins: "
+ + adminReceiver);
+ }
+ if (policy.mRemovingAdmins.contains(adminReceiver)) {
+ throw new IllegalArgumentException(
+ "Trying to set an admin which is being removed");
+ }
+ }
+
@Override
public boolean isAdminActive(ComponentName adminReceiver, int userHandle) {
if (!mHasFeature) {
@@ -3523,11 +3636,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
@Override
public int getPasswordQuality(ComponentName who, int userHandle, boolean parent) {
if (!mHasFeature) {
- return DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+ return PASSWORD_QUALITY_UNSPECIFIED;
}
enforceFullCrossUsersPermission(userHandle);
synchronized (this) {
- int mode = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+ int mode = PASSWORD_QUALITY_UNSPECIFIED;
if (who != null) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
@@ -3610,30 +3723,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
@Override
public int getPasswordMinimumLength(ComponentName who, int userHandle, boolean parent) {
- if (!mHasFeature) {
- return 0;
- }
- enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
- int length = 0;
-
- if (who != null) {
- ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
- return admin != null ? admin.minimumPasswordMetrics.length : length;
- }
-
- // Return the strictest policy across all participating admins.
- List<ActiveAdmin> admins =
- getActiveAdminsForLockscreenPoliciesLocked(userHandle, parent);
- final int N = admins.size();
- for (int i = 0; i < N; i++) {
- ActiveAdmin admin = admins.get(i);
- if (length < admin.minimumPasswordMetrics.length) {
- length = admin.minimumPasswordMetrics.length;
- }
- }
- return length;
- }
+ return getStrictestPasswordRequirement(who, userHandle, parent,
+ admin -> admin.minimumPasswordMetrics.length, PASSWORD_QUALITY_UNSPECIFIED);
}
@Override
@@ -3655,31 +3746,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
@Override
public int getPasswordHistoryLength(ComponentName who, int userHandle, boolean parent) {
- if (!mHasFeature) {
- return 0;
- }
- enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
- int length = 0;
-
- if (who != null) {
- ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
- return admin != null ? admin.passwordHistoryLength : length;
- }
-
- // Return the strictest policy across all participating admins.
- List<ActiveAdmin> admins =
- getActiveAdminsForLockscreenPoliciesLocked(userHandle, parent);
- final int N = admins.size();
- for (int i = 0; i < N; i++) {
- ActiveAdmin admin = admins.get(i);
- if (length < admin.passwordHistoryLength) {
- length = admin.passwordHistoryLength;
- }
- }
-
- return length;
- }
+ return getStrictestPasswordRequirement(who, userHandle, parent,
+ admin -> admin.passwordHistoryLength, PASSWORD_QUALITY_UNSPECIFIED);
}
@Override
@@ -3868,30 +3936,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
@Override
public int getPasswordMinimumUpperCase(ComponentName who, int userHandle, boolean parent) {
- if (!mHasFeature) {
- return 0;
- }
- enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
- int length = 0;
-
- if (who != null) {
- ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
- return admin != null ? admin.minimumPasswordMetrics.upperCase : length;
- }
-
- // Return the strictest policy across all participating admins.
- List<ActiveAdmin> admins =
- getActiveAdminsForLockscreenPoliciesLocked(userHandle, parent);
- final int N = admins.size();
- for (int i = 0; i < N; i++) {
- ActiveAdmin admin = admins.get(i);
- if (length < admin.minimumPasswordMetrics.upperCase) {
- length = admin.minimumPasswordMetrics.upperCase;
- }
- }
- return length;
- }
+ return getStrictestPasswordRequirement(who, userHandle, parent,
+ admin -> admin.minimumPasswordMetrics.upperCase, PASSWORD_QUALITY_COMPLEX);
}
@Override
@@ -3910,30 +3956,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
@Override
public int getPasswordMinimumLowerCase(ComponentName who, int userHandle, boolean parent) {
- if (!mHasFeature) {
- return 0;
- }
- enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
- int length = 0;
-
- if (who != null) {
- ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
- return admin != null ? admin.minimumPasswordMetrics.lowerCase : length;
- }
-
- // Return the strictest policy across all participating admins.
- List<ActiveAdmin> admins =
- getActiveAdminsForLockscreenPoliciesLocked(userHandle, parent);
- final int N = admins.size();
- for (int i = 0; i < N; i++) {
- ActiveAdmin admin = admins.get(i);
- if (length < admin.minimumPasswordMetrics.lowerCase) {
- length = admin.minimumPasswordMetrics.lowerCase;
- }
- }
- return length;
- }
+ return getStrictestPasswordRequirement(who, userHandle, parent,
+ admin -> admin.minimumPasswordMetrics.lowerCase, PASSWORD_QUALITY_COMPLEX);
}
@Override
@@ -3955,33 +3979,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
@Override
public int getPasswordMinimumLetters(ComponentName who, int userHandle, boolean parent) {
- if (!mHasFeature) {
- return 0;
- }
- enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
- int length = 0;
-
- if (who != null) {
- ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
- return admin != null ? admin.minimumPasswordMetrics.letters : length;
- }
-
- // Return the strictest policy across all participating admins.
- List<ActiveAdmin> admins =
- getActiveAdminsForLockscreenPoliciesLocked(userHandle, parent);
- final int N = admins.size();
- for (int i = 0; i < N; i++) {
- ActiveAdmin admin = admins.get(i);
- if (!isLimitPasswordAllowed(admin, PASSWORD_QUALITY_COMPLEX)) {
- continue;
- }
- if (length < admin.minimumPasswordMetrics.letters) {
- length = admin.minimumPasswordMetrics.letters;
- }
- }
- return length;
- }
+ return getStrictestPasswordRequirement(who, userHandle, parent,
+ admin -> admin.minimumPasswordMetrics.letters, PASSWORD_QUALITY_COMPLEX);
}
@Override
@@ -4003,37 +4002,35 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
@Override
public int getPasswordMinimumNumeric(ComponentName who, int userHandle, boolean parent) {
+ return getStrictestPasswordRequirement(who, userHandle, parent,
+ admin -> admin.minimumPasswordMetrics.numeric, PASSWORD_QUALITY_COMPLEX);
+ }
+
+ @Override
+ public void setPasswordMinimumSymbols(ComponentName who, int length, boolean parent) {
if (!mHasFeature) {
- return 0;
+ return;
}
- enforceFullCrossUsersPermission(userHandle);
+ Preconditions.checkNotNull(who, "ComponentName is null");
synchronized (this) {
- int length = 0;
-
- if (who != null) {
- ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
- return admin != null ? admin.minimumPasswordMetrics.numeric : length;
- }
-
- // Return the strictest policy across all participating admins.
- List<ActiveAdmin> admins =
- getActiveAdminsForLockscreenPoliciesLocked(userHandle, parent);
- final int N = admins.size();
- for (int i = 0; i < N; i++) {
- ActiveAdmin admin = admins.get(i);
- if (!isLimitPasswordAllowed(admin, PASSWORD_QUALITY_COMPLEX)) {
- continue;
- }
- if (length < admin.minimumPasswordMetrics.numeric) {
- length = admin.minimumPasswordMetrics.numeric;
- }
+ ActiveAdmin ap = getActiveAdminForCallerLocked(
+ who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
+ if (ap.minimumPasswordMetrics.symbols != length) {
+ ap.minimumPasswordMetrics.symbols = length;
+ updatePasswordValidityCheckpointLocked(mInjector.userHandleGetCallingUserId());
+ saveSettingsLocked(mInjector.userHandleGetCallingUserId());
}
- return length;
}
}
@Override
- public void setPasswordMinimumSymbols(ComponentName who, int length, boolean parent) {
+ public int getPasswordMinimumSymbols(ComponentName who, int userHandle, boolean parent) {
+ return getStrictestPasswordRequirement(who, userHandle, parent,
+ admin -> admin.minimumPasswordMetrics.symbols, PASSWORD_QUALITY_COMPLEX);
+ }
+
+ @Override
+ public void setPasswordMinimumNonLetter(ComponentName who, int length, boolean parent) {
if (!mHasFeature) {
return;
}
@@ -4041,8 +4038,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
synchronized (this) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
- if (ap.minimumPasswordMetrics.symbols != length) {
- ap.minimumPasswordMetrics.symbols = length;
+ if (ap.minimumPasswordMetrics.nonLetter != length) {
+ ap.minimumPasswordMetrics.nonLetter = length;
updatePasswordValidityCheckpointLocked(mInjector.userHandleGetCallingUserId());
saveSettingsLocked(mInjector.userHandleGetCallingUserId());
}
@@ -4050,82 +4047,172 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
@Override
- public int getPasswordMinimumSymbols(ComponentName who, int userHandle, boolean parent) {
+ public int getPasswordMinimumNonLetter(ComponentName who, int userHandle, boolean parent) {
+ return getStrictestPasswordRequirement(who, userHandle, parent,
+ admin -> admin.minimumPasswordMetrics.nonLetter, PASSWORD_QUALITY_COMPLEX);
+ }
+
+ /**
+ * Calculates strictest (maximum) value for a given password property enforced by admin[s].
+ */
+ private int getStrictestPasswordRequirement(ComponentName who, int userHandle,
+ boolean parent, Function<ActiveAdmin, Integer> getter, int minimumPasswordQuality) {
if (!mHasFeature) {
return 0;
}
enforceFullCrossUsersPermission(userHandle);
synchronized (this) {
- int length = 0;
-
if (who != null) {
- ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
- return admin != null ? admin.minimumPasswordMetrics.symbols : length;
+ final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
+ return admin != null ? getter.apply(admin) : 0;
}
- // Return the strictest policy across all participating admins.
- List<ActiveAdmin> admins =
+ int maxValue = 0;
+ final List<ActiveAdmin> admins =
getActiveAdminsForLockscreenPoliciesLocked(userHandle, parent);
final int N = admins.size();
for (int i = 0; i < N; i++) {
- ActiveAdmin admin = admins.get(i);
- if (!isLimitPasswordAllowed(admin, PASSWORD_QUALITY_COMPLEX)) {
+ final ActiveAdmin admin = admins.get(i);
+ if (!isLimitPasswordAllowed(admin, minimumPasswordQuality)) {
continue;
}
- if (length < admin.minimumPasswordMetrics.symbols) {
- length = admin.minimumPasswordMetrics.symbols;
+ final Integer adminValue = getter.apply(admin);
+ if (adminValue > maxValue) {
+ maxValue = adminValue;
}
}
- return length;
+ return maxValue;
}
}
+ /* @return the password blacklist set by the admin or {@code null} if none. */
+ PasswordBlacklist getAdminPasswordBlacklistLocked(@NonNull ActiveAdmin admin) {
+ final int userId = UserHandle.getUserId(admin.getUid());
+ return admin.passwordBlacklistFile == null ? null : new PasswordBlacklist(
+ new File(getPolicyFileDirectory(userId), admin.passwordBlacklistFile));
+ }
+
+ private static final String PASSWORD_BLACKLIST_FILE_PREFIX = "password-blacklist-";
+ private static final String PASSWORD_BLACKLIST_FILE_SUFFIX = "";
+
@Override
- public void setPasswordMinimumNonLetter(ComponentName who, int length, boolean parent) {
+ public boolean setPasswordBlacklist(ComponentName who, String name, List<String> blacklist,
+ boolean parent) {
if (!mHasFeature) {
- return;
+ return false;
}
- Preconditions.checkNotNull(who, "ComponentName is null");
+ Preconditions.checkNotNull(who, "who is null");
+
synchronized (this) {
- ActiveAdmin ap = getActiveAdminForCallerLocked(
- who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
- if (ap.minimumPasswordMetrics.nonLetter != length) {
- ap.minimumPasswordMetrics.nonLetter = length;
- updatePasswordValidityCheckpointLocked(mInjector.userHandleGetCallingUserId());
- saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+ final ActiveAdmin admin = getActiveAdminForCallerLocked(
+ who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, parent);
+ final int userId = mInjector.userHandleGetCallingUserId();
+ PasswordBlacklist adminBlacklist = getAdminPasswordBlacklistLocked(admin);
+
+ if (blacklist == null || blacklist.isEmpty()) {
+ // Remove the adminBlacklist
+ admin.passwordBlacklistFile = null;
+ saveSettingsLocked(userId);
+ if (adminBlacklist != null) {
+ adminBlacklist.delete();
+ }
+ return true;
+ }
+
+ // Validate server side
+ Preconditions.checkNotNull(name, "name is null");
+ DevicePolicyManager.enforcePasswordBlacklistSize(blacklist);
+
+ // Blacklist is case insensitive so normalize to lower case
+ final int blacklistSize = blacklist.size();
+ for (int i = 0; i < blacklistSize; ++i) {
+ blacklist.set(i, blacklist.get(i).toLowerCase());
+ }
+
+ final boolean isNewBlacklist = adminBlacklist == null;
+ if (isNewBlacklist) {
+ // Create a new file for the blacklist. There could be multiple admins, each setting
+ // different blacklists, to restrict a user's credential, for example a managed
+ // profile can impose restrictions on its parent while the parent is already
+ // restricted by its own admin. A deterministic naming scheme would be fragile if
+ // new types of admin are introduced so we generate and save the file name instead.
+ // This isn't a temporary file but it reuses the name generation logic
+ final File file;
+ try {
+ file = File.createTempFile(PASSWORD_BLACKLIST_FILE_PREFIX,
+ PASSWORD_BLACKLIST_FILE_SUFFIX, getPolicyFileDirectory(userId));
+ } catch (IOException e) {
+ Slog.e(LOG_TAG, "Failed to make a file for the blacklist", e);
+ return false;
+ }
+ adminBlacklist = mInjector.newPasswordBlacklist(file);
+ }
+
+ if (adminBlacklist.savePasswordBlacklist(name, blacklist)) {
+ if (isNewBlacklist) {
+ // The blacklist was saved so point the admin to the file
+ admin.passwordBlacklistFile = adminBlacklist.getFile().getName();
+ saveSettingsLocked(userId);
+ }
+ return true;
}
}
+
+ return false;
}
@Override
- public int getPasswordMinimumNonLetter(ComponentName who, int userHandle, boolean parent) {
+ public String getPasswordBlacklistName(ComponentName who, @UserIdInt int userId,
+ boolean parent) {
if (!mHasFeature) {
- return 0;
+ return null;
}
- enforceFullCrossUsersPermission(userHandle);
+ Preconditions.checkNotNull(who, "who is null");
+ enforceFullCrossUsersPermission(userId);
synchronized (this) {
- int length = 0;
-
- if (who != null) {
- ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
- return admin != null ? admin.minimumPasswordMetrics.nonLetter : length;
+ final ActiveAdmin admin = getActiveAdminForCallerLocked(
+ who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, parent);
+ final PasswordBlacklist blacklist = getAdminPasswordBlacklistLocked(admin);
+ if (blacklist == null) {
+ return null;
}
+ return blacklist.getName();
+ }
+ }
- // Return the strictest policy across all participating admins.
- List<ActiveAdmin> admins =
- getActiveAdminsForLockscreenPoliciesLocked(userHandle, parent);
+ @Override
+ public boolean isPasswordBlacklisted(@UserIdInt int userId, String password) {
+ if (!mHasFeature) {
+ return false;
+ }
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.TEST_BLACKLISTED_PASSWORD, null);
+ return isPasswordBlacklistedInternal(userId, password);
+ }
+
+ private boolean isPasswordBlacklistedInternal(@UserIdInt int userId, String password) {
+ Preconditions.checkNotNull(password, "Password is null");
+ enforceFullCrossUsersPermission(userId);
+
+ // Normalize to lower case for case insensitive blacklist match
+ final String lowerCasePassword = password.toLowerCase();
+
+ synchronized (this) {
+ final List<ActiveAdmin> admins =
+ getActiveAdminsForLockscreenPoliciesLocked(userId, /* parent */ false);
final int N = admins.size();
for (int i = 0; i < N; i++) {
- ActiveAdmin admin = admins.get(i);
- if (!isLimitPasswordAllowed(admin, PASSWORD_QUALITY_COMPLEX)) {
- continue;
- }
- if (length < admin.minimumPasswordMetrics.nonLetter) {
- length = admin.minimumPasswordMetrics.nonLetter;
+ final PasswordBlacklist blacklist
+ = getAdminPasswordBlacklistLocked(admins.get(i));
+ if (blacklist != null) {
+ if (blacklist.isPasswordBlacklisted(lowerCasePassword)) {
+ return true;
+ }
}
}
- return length;
}
+
+ return false;
}
@Override
@@ -4146,6 +4233,17 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
@Override
+ public boolean isUsingUnifiedPassword(ComponentName admin) {
+ if (!mHasFeature) {
+ return true;
+ }
+ final int userId = mInjector.userHandleGetCallingUserId();
+ enforceProfileOrDeviceOwner(admin);
+ enforceManagedProfile(userId, "query unified challenge status");
+ return !isSeparateProfileChallengeEnabled(userId);
+ }
+
+ @Override
public boolean isProfileActivePasswordSufficientForParent(int userHandle) {
if (!mHasFeature) {
return true;
@@ -4164,7 +4262,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
private boolean isActivePasswordSufficientForUserLocked(
DevicePolicyData policy, int userHandle, boolean parent) {
final int requiredPasswordQuality = getPasswordQuality(null, userHandle, parent);
- if (requiredPasswordQuality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
+ if (requiredPasswordQuality == PASSWORD_QUALITY_UNSPECIFIED) {
// A special case is when there is no required password quality, then we just return
// true since any password would be sufficient. This is for the scenario when a work
// profile is first created so there is no information about the current password but
@@ -4404,10 +4502,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
synchronized (this) {
quality = getPasswordQuality(null, userHandle, /* parent */ false);
if (quality == DevicePolicyManager.PASSWORD_QUALITY_MANAGED) {
- quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+ quality = PASSWORD_QUALITY_UNSPECIFIED;
}
final PasswordMetrics metrics = PasswordMetrics.computeForPassword(password);
- if (quality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
+ if (quality != PASSWORD_QUALITY_UNSPECIFIED) {
final int realQuality = metrics.quality;
if (realQuality < quality
&& quality != DevicePolicyManager.PASSWORD_QUALITY_COMPLEX) {
@@ -4473,6 +4571,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
return false;
}
}
+
+ if (isPasswordBlacklistedInternal(userHandle, password)) {
+ Slog.w(LOG_TAG, "resetPassword: the password is blacklisted");
+ return false;
+ }
}
DevicePolicyData policy = getUserData(userHandle);
@@ -4571,56 +4674,56 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
}
- void updateMaximumTimeToLockLocked(int userHandle) {
- // Calculate the min timeout for all profiles - including the ones with a separate
- // challenge. Ideally if the timeout only affected the profile challenge we'd lock that
- // challenge only and keep the screen on. However there is no easy way of doing that at the
- // moment so we set the screen off timeout regardless of whether it affects the parent user
- // or the profile challenge only.
- long timeMs = Long.MAX_VALUE;
- int[] profileIds = mUserManager.getProfileIdsWithDisabled(userHandle);
- for (int profileId : profileIds) {
- DevicePolicyData policy = getUserDataUnchecked(profileId);
- final int N = policy.mAdminList.size();
- for (int i = 0; i < N; i++) {
- ActiveAdmin admin = policy.mAdminList.get(i);
- if (admin.maximumTimeToUnlock > 0
- && timeMs > admin.maximumTimeToUnlock) {
- timeMs = admin.maximumTimeToUnlock;
- }
- // If userInfo.id is a managed profile, we also need to look at
- // the policies set on the parent.
- if (admin.hasParentActiveAdmin()) {
- final ActiveAdmin parentAdmin = admin.getParentActiveAdmin();
- if (parentAdmin.maximumTimeToUnlock > 0
- && timeMs > parentAdmin.maximumTimeToUnlock) {
- timeMs = parentAdmin.maximumTimeToUnlock;
- }
- }
- }
- }
-
- // We only store the last maximum time to lock on the parent profile. So if calling from a
- // managed profile, retrieve the policy for the parent.
- DevicePolicyData policy = getUserDataUnchecked(getProfileParentId(userHandle));
- if (policy.mLastMaximumTimeToLock == timeMs) {
- return;
+ private void updateMaximumTimeToLockLocked(@UserIdInt int userId) {
+ // Update the profile's timeout
+ if (isManagedProfile(userId)) {
+ updateProfileLockTimeoutLocked(userId);
}
- policy.mLastMaximumTimeToLock = timeMs;
+ final long timeMs;
final long ident = mInjector.binderClearCallingIdentity();
try {
+ // Update the device timeout
+ final int parentId = getProfileParentId(userId);
+ timeMs = getMaximumTimeToLockPolicyFromAdmins(
+ getActiveAdminsForLockscreenPoliciesLocked(parentId, false));
+
+ final DevicePolicyData policy = getUserDataUnchecked(parentId);
+ if (policy.mLastMaximumTimeToLock == timeMs) {
+ return;
+ }
+ policy.mLastMaximumTimeToLock = timeMs;
+
if (policy.mLastMaximumTimeToLock != Long.MAX_VALUE) {
// Make sure KEEP_SCREEN_ON is disabled, since that
// would allow bypassing of the maximum time to lock.
mInjector.settingsGlobalPutInt(Settings.Global.STAY_ON_WHILE_PLUGGED_IN, 0);
}
-
- mInjector.getPowerManagerInternal().setMaximumScreenOffTimeoutFromDeviceAdmin(
- (int) Math.min(policy.mLastMaximumTimeToLock, Integer.MAX_VALUE));
} finally {
mInjector.binderRestoreCallingIdentity(ident);
}
+
+ mInjector.getPowerManagerInternal().setMaximumScreenOffTimeoutFromDeviceAdmin(
+ UserHandle.USER_SYSTEM, timeMs);
+ }
+
+ private void updateProfileLockTimeoutLocked(@UserIdInt int userId) {
+ final long timeMs;
+ if (isSeparateProfileChallengeEnabled(userId)) {
+ timeMs = getMaximumTimeToLockPolicyFromAdmins(
+ getActiveAdminsForLockscreenPoliciesLocked(userId, false /* parent */));
+ } else {
+ timeMs = Long.MAX_VALUE;
+ }
+
+ final DevicePolicyData policy = getUserDataUnchecked(userId);
+ if (policy.mLastMaximumTimeToLock == timeMs) {
+ return;
+ }
+ policy.mLastMaximumTimeToLock = timeMs;
+
+ mInjector.getPowerManagerInternal().setMaximumScreenOffTimeoutFromDeviceAdmin(
+ userId, policy.mLastMaximumTimeToLock);
}
@Override
@@ -4631,50 +4734,21 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
enforceFullCrossUsersPermission(userHandle);
synchronized (this) {
if (who != null) {
- ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
+ final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
return admin != null ? admin.maximumTimeToUnlock : 0;
}
// Return the strictest policy across all participating admins.
- List<ActiveAdmin> admins = getActiveAdminsForLockscreenPoliciesLocked(
+ final List<ActiveAdmin> admins = getActiveAdminsForLockscreenPoliciesLocked(
userHandle, parent);
- return getMaximumTimeToLockPolicyFromAdmins(admins);
- }
- }
-
- @Override
- public long getMaximumTimeToLockForUserAndProfiles(int userHandle) {
- if (!mHasFeature) {
- return 0;
- }
- enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
- // All admins for this user.
- ArrayList<ActiveAdmin> admins = new ArrayList<ActiveAdmin>();
- for (UserInfo userInfo : mUserManager.getProfiles(userHandle)) {
- DevicePolicyData policy = getUserData(userInfo.id);
- admins.addAll(policy.mAdminList);
- // If it is a managed profile, it may have parent active admins
- if (userInfo.isManagedProfile()) {
- for (ActiveAdmin admin : policy.mAdminList) {
- if (admin.hasParentActiveAdmin()) {
- admins.add(admin.getParentActiveAdmin());
- }
- }
- }
- }
- return getMaximumTimeToLockPolicyFromAdmins(admins);
+ final long timeMs = getMaximumTimeToLockPolicyFromAdmins(admins);
+ return timeMs == Long.MAX_VALUE ? 0 : timeMs;
}
}
private long getMaximumTimeToLockPolicyFromAdmins(List<ActiveAdmin> admins) {
- long time = 0;
- final int N = admins.size();
- for (int i = 0; i < N; i++) {
- ActiveAdmin admin = admins.get(i);
- if (time == 0) {
- time = admin.maximumTimeToUnlock;
- } else if (admin.maximumTimeToUnlock != 0
- && time > admin.maximumTimeToUnlock) {
+ long time = Long.MAX_VALUE;
+ for (final ActiveAdmin admin : admins) {
+ if (admin.maximumTimeToUnlock > 0 && admin.maximumTimeToUnlock < time) {
time = admin.maximumTimeToUnlock;
}
}
@@ -4979,6 +5053,105 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
@Override
+ public boolean generateKeyPair(ComponentName who, String callerPackage, String algorithm,
+ ParcelableKeyGenParameterSpec parcelableKeySpec,
+ KeymasterCertificateChain attestationChain) {
+ enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
+ DELEGATION_CERT_INSTALL);
+ final KeyGenParameterSpec keySpec = parcelableKeySpec.getSpec();
+ if (TextUtils.isEmpty(keySpec.getKeystoreAlias())) {
+ throw new IllegalArgumentException("Empty alias provided.");
+ }
+ final String alias = keySpec.getKeystoreAlias();
+ // As the caller will be granted access to the key, ensure no UID was specified, as
+ // it will not have the desired effect.
+ if (keySpec.getUid() != KeyStore.UID_SELF) {
+ Log.e(LOG_TAG, "Only the caller can be granted access to the generated keypair.");
+ return false;
+ }
+
+ final int callingUid = mInjector.binderGetCallingUid();
+
+ final UserHandle userHandle = mInjector.binderGetCallingUserHandle();
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ try (KeyChainConnection keyChainConnection =
+ KeyChain.bindAsUser(mContext, userHandle)) {
+ IKeyChainService keyChain = keyChainConnection.getService();
+
+ // Copy the provided keySpec, excluding the attestation challenge, which will be
+ // used later for requesting key attestation record.
+ final KeyGenParameterSpec noAttestationSpec =
+ new KeyGenParameterSpec.Builder(keySpec)
+ .setAttestationChallenge(null)
+ .build();
+
+ final boolean generationResult = keyChain.generateKeyPair(algorithm,
+ new ParcelableKeyGenParameterSpec(noAttestationSpec));
+ if (!generationResult) {
+ Log.e(LOG_TAG, "KeyChain failed to generate a keypair.");
+ return false;
+ }
+
+ // Set a grant for the caller here so that when the client calls
+ // requestPrivateKey, it will be able to get the key from Keystore.
+ // Note the use of the calling UID, since the request for the private
+ // key will come from the client's process, so the grant has to be for
+ // that UID.
+ keyChain.setGrant(callingUid, alias, true);
+
+ final byte[] attestationChallenge = keySpec.getAttestationChallenge();
+ if (attestationChallenge != null) {
+ final boolean attestationResult = keyChain.attestKey(
+ alias, attestationChallenge, attestationChain);
+ if (!attestationResult) {
+ Log.e(LOG_TAG, String.format(
+ "Attestation for %s failed, deleting key.", alias));
+ keyChain.removeKeyPair(alias);
+ return false;
+ }
+ }
+ return true;
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "KeyChain error while generating a keypair", e);
+ } catch (InterruptedException e) {
+ Log.w(LOG_TAG, "Interrupted while generating keypair", e);
+ Thread.currentThread().interrupt();
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean setKeyPairCertificate(ComponentName who, String callerPackage, String alias,
+ byte[] cert, byte[] chain, boolean isUserSelectable) {
+ enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
+ DELEGATION_CERT_INSTALL);
+
+ final int callingUid = mInjector.binderGetCallingUid();
+ final long id = mInjector.binderClearCallingIdentity();
+ try (final KeyChainConnection keyChainConnection =
+ KeyChain.bindAsUser(mContext, UserHandle.getUserHandleForUid(callingUid))) {
+ IKeyChainService keyChain = keyChainConnection.getService();
+ if (!keyChain.setKeyPairCertificate(alias, cert, chain)) {
+ return false;
+ }
+ keyChain.setUserSelectable(alias, isUserSelectable);
+ return true;
+ } catch (InterruptedException e) {
+ Log.w(LOG_TAG, "Interrupted while setting keypair certificate", e);
+ Thread.currentThread().interrupt();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Failed setting keypair certificate", e);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ return false;
+ }
+
+ @Override
public void choosePrivateKeyAlias(final int uid, final Uri uri, final String alias,
final IBinder response) {
// Caller UID needs to be trusted, so we restrict this method to SYSTEM_UID callers.
@@ -6943,8 +7116,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
return;
}
+ final int userId = mInjector.userHandleGetCallingUserId();
synchronized (this) {
- getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ if (!isUserAffiliatedWithDeviceLocked(userId)) {
+ throw new SecurityException("Admin " + who +
+ " is neither the device owner or affiliated user's profile owner.");
+ }
long token = mInjector.binderClearCallingIdentity();
try {
mLockPatternUtils.setDeviceOwnerInfo(info != null ? info.toString() : null);
@@ -8220,6 +8398,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
final boolean ephemeral = (flags & DevicePolicyManager.MAKE_USER_EPHEMERAL) != 0;
final boolean demo = (flags & DevicePolicyManager.MAKE_USER_DEMO) != 0
&& UserManager.isDeviceInDemoMode(mContext);
+ final boolean leaveAllSystemAppsEnabled = (flags & LEAVE_ALL_SYSTEM_APPS_ENABLED) != 0;
// Create user.
UserHandle user = null;
synchronized (this) {
@@ -8234,8 +8413,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
if (demo) {
userInfoFlags |= UserInfo.FLAG_DEMO;
}
+ String[] disallowedPackages = null;
+ if (!leaveAllSystemAppsEnabled) {
+ disallowedPackages = mOverlayPackagesProvider.getNonRequiredApps(admin,
+ UserHandle.myUserId(), ACTION_PROVISION_MANAGED_USER).toArray(
+ new String[0]);
+ }
UserInfo userInfo = mUserManagerInternal.createUserEvenWhenDisallowed(name,
- userInfoFlags);
+ userInfoFlags, disallowedPackages);
if (userInfo != null) {
user = userInfo.getUserHandle();
}
@@ -8246,11 +8431,20 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
if (user == null) {
return null;
}
+
+ final int userHandle = user.getIdentifier();
+ final Intent intent = new Intent(DevicePolicyManager.ACTION_MANAGED_USER_CREATED)
+ .putExtra(Intent.EXTRA_USER_HANDLE, userHandle)
+ .putExtra(
+ DevicePolicyManager.EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED,
+ leaveAllSystemAppsEnabled)
+ .setPackage(MANAGED_PROVISIONING_PKG)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+
final long id = mInjector.binderClearCallingIdentity();
try {
final String adminPkg = admin.getPackageName();
-
- final int userHandle = user.getIdentifier();
try {
// Install the profile owner if not present.
if (!mIPackageManager.isPackageAvailable(adminPkg, userHandle)) {
@@ -8280,7 +8474,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
if ((flags & START_USER_IN_BACKGROUND) != 0) {
try {
- mInjector.getIActivityManager().startUserInBackground(user.getIdentifier());
+ mInjector.getIActivityManager().startUserInBackground(userHandle);
} catch (RemoteException re) {
// Does not happen, same process
}
@@ -8288,7 +8482,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
return user;
} catch (Throwable re) {
- mUserManager.removeUser(user.getIdentifier());
+ mUserManager.removeUser(userHandle);
return null;
} finally {
mInjector.binderRestoreCallingIdentity(id);
@@ -8356,6 +8550,106 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
@Override
+ public boolean stopUser(ComponentName who, UserHandle userHandle) {
+ Preconditions.checkNotNull(who, "ComponentName is null");
+
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
+
+ final int userId = userHandle.getIdentifier();
+ if (isManagedProfile(userId)) {
+ Log.w(LOG_TAG, "Managed profile cannot be stopped");
+ return false;
+ }
+
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ return mInjector.getIActivityManager().stopUser(userId, true /*force*/, null)
+ == USER_OP_SUCCESS;
+ } catch (RemoteException e) {
+ // Same process, should not happen.
+ return false;
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ @Override
+ public boolean logoutUser(ComponentName who) {
+ Preconditions.checkNotNull(who, "ComponentName is null");
+
+ final int callingUserId = mInjector.userHandleGetCallingUserId();
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ if (!isUserAffiliatedWithDeviceLocked(callingUserId)) {
+ throw new SecurityException("Admin " + who +
+ " is neither the device owner or affiliated user's profile owner.");
+ }
+ }
+
+ if (isManagedProfile(callingUserId)) {
+ Log.w(LOG_TAG, "Managed profile cannot be logout");
+ return false;
+ }
+
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ if (!mInjector.getIActivityManager().switchUser(UserHandle.USER_SYSTEM)) {
+ Log.w(LOG_TAG, "Failed to switch to primary user");
+ return false;
+ }
+ return mInjector.getIActivityManager().stopUser(callingUserId, true /*force*/, null)
+ == USER_OP_SUCCESS;
+ } catch (RemoteException e) {
+ // Same process, should not happen.
+ return false;
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ @Override
+ public List<UserHandle> getSecondaryUsers(ComponentName who) {
+ Preconditions.checkNotNull(who, "ComponentName is null");
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
+
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ final List<UserInfo> userInfos = mInjector.getUserManager().getUsers(true
+ /*excludeDying*/);
+ final List<UserHandle> userHandles = new ArrayList<>();
+ for (UserInfo userInfo : userInfos) {
+ UserHandle userHandle = userInfo.getUserHandle();
+ if (!userHandle.isSystem() && !isManagedProfile(userHandle.getIdentifier())) {
+ userHandles.add(userInfo.getUserHandle());
+ }
+ }
+ return userHandles;
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ @Override
+ public boolean isEphemeralUser(ComponentName who) {
+ Preconditions.checkNotNull(who, "ComponentName is null");
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ }
+
+ final int callingUserId = mInjector.userHandleGetCallingUserId();
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ return mInjector.getUserManager().isUserEphemeral(callingUserId);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ @Override
public Bundle getApplicationRestrictions(ComponentName who, String callerPackage,
String packageName) {
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
@@ -8661,6 +8955,39 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
@Override
+ public boolean installExistingPackage(ComponentName who, String callerPackage,
+ String packageName) {
+ synchronized (this) {
+ // Ensure the caller is a PO or an install existing package delegate
+ enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
+ DELEGATION_INSTALL_EXISTING_PACKAGE);
+ final int callingUserId = mInjector.userHandleGetCallingUserId();
+ if (!isUserAffiliatedWithDeviceLocked(callingUserId)) {
+ throw new SecurityException("Admin " + who +
+ " is neither the device owner or affiliated user's profile owner.");
+ }
+
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ if (VERBOSE_LOG) {
+ Slog.v(LOG_TAG, "installing " + packageName + " for "
+ + callingUserId);
+ }
+
+ // Install the package.
+ return mIPackageManager.installExistingPackageAsUser(packageName, callingUserId,
+ 0 /*installFlags*/, PackageManager.INSTALL_REASON_POLICY)
+ == PackageManager.INSTALL_SUCCEEDED;
+ } catch (RemoteException re) {
+ // shouldn't happen
+ return false;
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+ }
+
+ @Override
public void setAccountManagementDisabled(ComponentName who, String accountType,
boolean disabled) {
if (!mHasFeature) {
@@ -9095,7 +9422,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
// ignore if it contradicts an existing policy
long timeMs = getMaximumTimeToLock(
who, mInjector.userHandleGetCallingUserId(), /* parent */ false);
- if (timeMs > 0 && timeMs < Integer.MAX_VALUE) {
+ if (timeMs > 0 && timeMs < Long.MAX_VALUE) {
return;
}
}
@@ -9110,6 +9437,24 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
@Override
+ public void setSystemSetting(ComponentName who, String setting, String value) {
+ Preconditions.checkNotNull(who, "ComponentName is null");
+ Preconditions.checkStringNotEmpty(setting, "String setting is null or empty");
+
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+
+ if (!SYSTEM_SETTINGS_WHITELIST.contains(setting)) {
+ throw new SecurityException(String.format(
+ "Permission denial: device owners cannot update %1$s", setting));
+ }
+
+ mInjector.binderWithCleanCallingIdentity(() -> mInjector.settingsSystemPutString(
+ setting, value));
+ }
+ }
+
+ @Override
public boolean setTime(ComponentName who, long millis) {
Preconditions.checkNotNull(who, "ComponentName is null in setTime");
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -9236,10 +9581,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
@Override
public boolean setKeyguardDisabled(ComponentName who, boolean disabled) {
Preconditions.checkNotNull(who, "ComponentName is null");
+ final int userId = mInjector.userHandleGetCallingUserId();
synchronized (this) {
- getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ if (!isUserAffiliatedWithDeviceLocked(userId)) {
+ throw new SecurityException("Admin " + who +
+ " is neither the device owner or affiliated user's profile owner.");
+ }
}
- final int userId = UserHandle.getCallingUserId();
long ident = mInjector.binderClearCallingIdentity();
try {
@@ -9261,7 +9610,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
public boolean setStatusBarDisabled(ComponentName who, boolean disabled) {
int userId = UserHandle.getCallingUserId();
synchronized (this) {
- getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ if (!isUserAffiliatedWithDeviceLocked(userId)) {
+ throw new SecurityException("Admin " + who +
+ " is neither the device owner or affiliated user's profile owner.");
+ }
DevicePolicyData policy = getUserData(userId);
if (policy.mStatusBarDisabled != disabled) {
boolean isLockTaskMode = false;
@@ -9526,6 +9879,18 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
return null;
}
+
+ @Override
+ public boolean isUserAffiliatedWithDevice(int userId) {
+ return DevicePolicyManagerService.this.isUserAffiliatedWithDeviceLocked(userId);
+ }
+
+ @Override
+ public void reportSeparateProfileChallengeChanged(@UserIdInt int userId) {
+ synchronized (DevicePolicyManagerService.this) {
+ updateMaximumTimeToLockLocked(userId);
+ }
+ }
}
private Intent createShowAdminSupportIntent(ComponentName admin, int userId) {
@@ -11341,8 +11706,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
long ident = mInjector.binderClearCallingIdentity();
try {
- return ActivityManager.getService().clearApplicationUserData(packageName, callback,
- userId);
+ return ActivityManager.getService().clearApplicationUserData(packageName, false,
+ callback, userId);
} catch(RemoteException re) {
// Same process, should not happen.
} catch (SecurityException se) {
@@ -11365,4 +11730,100 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
return false;
}
+
+ @Override
+ public synchronized void setLogoutEnabled(ComponentName admin, boolean enabled) {
+ if (!mHasFeature) {
+ return;
+ }
+ Preconditions.checkNotNull(admin);
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+
+ if (enabled == isLogoutEnabledInternalLocked()) {
+ // already in the requested state
+ return;
+ }
+ ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+ deviceOwner.isLogoutEnabled = enabled;
+ saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+ }
+
+ @Override
+ public boolean isLogoutEnabled() {
+ if (!mHasFeature) {
+ return false;
+ }
+ synchronized (this) {
+ return isLogoutEnabledInternalLocked();
+ }
+ }
+
+ private boolean isLogoutEnabledInternalLocked() {
+ ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+ return (deviceOwner != null) && deviceOwner.isLogoutEnabled;
+ }
+
+ @Override
+ public List<String> getDisallowedSystemApps(ComponentName admin, int userId,
+ String provisioningAction) throws RemoteException {
+ enforceCanManageProfileAndDeviceOwners();
+ return new ArrayList<>(
+ mOverlayPackagesProvider.getNonRequiredApps(admin, userId, provisioningAction));
+ }
+
+ //TODO: Add callback information to the javadoc once it is completed.
+ //TODO: Make transferOwner atomic.
+ @Override
+ public void transferOwner(ComponentName admin, ComponentName target, PersistableBundle bundle) {
+ if (!mHasFeature) {
+ return;
+ }
+
+ Preconditions.checkNotNull(admin, "Admin cannot be null.");
+ Preconditions.checkNotNull(target, "Target cannot be null.");
+
+ enforceProfileOrDeviceOwner(admin);
+
+ if (admin.equals(target)) {
+ throw new IllegalArgumentException("Provided administrator and target are "
+ + "the same object.");
+ }
+
+ if (admin.getPackageName().equals(target.getPackageName())) {
+ throw new IllegalArgumentException("Provided administrator and target have "
+ + "the same package name.");
+ }
+
+ final int callingUserId = mInjector.userHandleGetCallingUserId();
+ final DevicePolicyData policy = getUserData(callingUserId);
+ final DeviceAdminInfo incomingDeviceInfo = findAdmin(target, callingUserId,
+ /* throwForMissingPermission= */ true);
+ checkActiveAdminPrecondition(target, incomingDeviceInfo, policy);
+
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ //STOPSHIP add support for COMP, DO, edge cases when device is rebooted/work mode off,
+ //transfer callbacks and broadcast
+ if (isProfileOwner(admin, callingUserId)) {
+ transferProfileOwner(admin, target, callingUserId);
+ }
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ /**
+ * Transfers the profile owner for user with id profileOwnerUserId from admin to target.
+ */
+ private void transferProfileOwner(ComponentName admin, ComponentName target,
+ int profileOwnerUserId) {
+ synchronized (this) {
+ transferActiveAdminUncheckedLocked(target, admin, profileOwnerUserId);
+ mOwners.transferProfileOwner(target, profileOwnerUserId);
+ Slog.i(LOG_TAG, "Profile owner set: " + target + " on user " + profileOwnerUserId);
+ mOwners.writeProfileOwner(profileOwnerUserId);
+ mDeviceAdminServiceController.startServiceForOwner(
+ target.getPackageName(), profileOwnerUserId, "transfer-profile-owner");
+ }
+ }
}
diff --git a/com/android/server/devicepolicy/NetworkLoggingHandler.java b/com/android/server/devicepolicy/NetworkLoggingHandler.java
index 4a6bee5c..f91f959d 100644
--- a/com/android/server/devicepolicy/NetworkLoggingHandler.java
+++ b/com/android/server/devicepolicy/NetworkLoggingHandler.java
@@ -29,10 +29,10 @@ import android.util.LongSparseArray;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.TimeUnit;
/**
* A Handler class for managing network logging on a background thread.
@@ -81,6 +81,7 @@ final class NetworkLoggingHandler extends Handler {
}
};
+ @VisibleForTesting
static final int LOG_NETWORK_EVENT_MSG = 1;
/** Network events accumulated so far to be finalized into a batch at some point. */
@@ -106,9 +107,15 @@ final class NetworkLoggingHandler extends Handler {
private long mLastRetrievedBatchToken;
NetworkLoggingHandler(Looper looper, DevicePolicyManagerService dpm) {
+ this(looper, dpm, 0 /* event id */);
+ }
+
+ @VisibleForTesting
+ NetworkLoggingHandler(Looper looper, DevicePolicyManagerService dpm, long id) {
super(looper);
- mDpm = dpm;
- mAlarmManager = mDpm.mInjector.getAlarmManager();
+ this.mDpm = dpm;
+ this.mAlarmManager = mDpm.mInjector.getAlarmManager();
+ this.mId = id;
}
@Override
@@ -189,7 +196,13 @@ final class NetworkLoggingHandler extends Handler {
if (mNetworkEvents.size() > 0) {
// Assign ids to the events.
for (NetworkEvent event : mNetworkEvents) {
- event.setId(mId++);
+ event.setId(mId);
+ if (mId == Long.MAX_VALUE) {
+ Slog.i(TAG, "Reached maximum id value; wrapping around ." + mCurrentBatchToken);
+ mId = 0;
+ } else {
+ mId++;
+ }
}
// Finalize the batch and start a new one from scratch.
if (mBatches.size() >= MAX_BATCHES) {
diff --git a/com/android/server/devicepolicy/OverlayPackagesProvider.java b/com/android/server/devicepolicy/OverlayPackagesProvider.java
new file mode 100644
index 00000000..d0ec0eee
--- /dev/null
+++ b/com/android/server/devicepolicy/OverlayPackagesProvider.java
@@ -0,0 +1,232 @@
+/*
+ * 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 com.android.server.devicepolicy;
+
+import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE;
+import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE;
+import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_USER;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.app.admin.DeviceAdminReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.ArraySet;
+import android.view.inputmethod.InputMethodInfo;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.view.IInputMethodManager;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Class that provides the apps that are not required on a managed device / profile according to the
+ * overlays provided via (vendor_|)required_apps_managed_(profile|device).xml.
+ */
+public class OverlayPackagesProvider {
+
+ protected static final String TAG = "OverlayPackagesProvider";
+
+ private final PackageManager mPm;
+ private final IInputMethodManager mIInputMethodManager;
+ private final Context mContext;
+
+ public OverlayPackagesProvider(Context context) {
+ this(context, getIInputMethodManager());
+ }
+
+ @VisibleForTesting
+ OverlayPackagesProvider(Context context, IInputMethodManager iInputMethodManager) {
+ mContext = context;
+ mPm = checkNotNull(context.getPackageManager());
+ mIInputMethodManager = checkNotNull(iInputMethodManager);
+ }
+
+ /**
+ * Computes non-required apps. All the system apps with a launcher that are not in
+ * the required set of packages will be considered as non-required apps.
+ *
+ * Note: If an app is mistakenly listed as both required and disallowed, it will be treated as
+ * disallowed.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param userId The userId for which the non-required apps needs to be computed.
+ * @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}.
+ * @return the set of non-required apps.
+ */
+ @NonNull
+ public Set<String> getNonRequiredApps(@NonNull ComponentName admin, int userId,
+ @NonNull String provisioningAction) {
+ final Set<String> nonRequiredApps = getLaunchableApps(userId);
+ // Newly installed system apps are uninstalled when they are not required and are either
+ // disallowed or have a launcher icon.
+ nonRequiredApps.removeAll(getRequiredApps(provisioningAction, admin.getPackageName()));
+ // Don't delete the system input method packages in case of Device owner provisioning.
+ if (ACTION_PROVISION_MANAGED_DEVICE.equals(provisioningAction)
+ || ACTION_PROVISION_MANAGED_USER.equals(provisioningAction)) {
+ nonRequiredApps.removeAll(getSystemInputMethods());
+ }
+ nonRequiredApps.addAll(getDisallowedApps(provisioningAction));
+ return nonRequiredApps;
+ }
+
+ private Set<String> getLaunchableApps(int userId) {
+ final Intent launcherIntent = new Intent(Intent.ACTION_MAIN);
+ launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+ final List<ResolveInfo> resolveInfos = mPm.queryIntentActivitiesAsUser(launcherIntent,
+ PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+ userId);
+ final Set<String> apps = new ArraySet<>();
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ apps.add(resolveInfo.activityInfo.packageName);
+ }
+ return apps;
+ }
+
+ private Set<String> getSystemInputMethods() {
+ // InputMethodManager is final so it cannot be mocked.
+ // So, we're using IInputMethodManager directly because it can be mocked.
+ final List<InputMethodInfo> inputMethods;
+ try {
+ inputMethods = mIInputMethodManager.getInputMethodList();
+ } catch (RemoteException e) {
+ // Should not happen
+ return null;
+ }
+ final Set<String> systemInputMethods = new ArraySet<>();
+ for (InputMethodInfo inputMethodInfo : inputMethods) {
+ ApplicationInfo applicationInfo = inputMethodInfo.getServiceInfo().applicationInfo;
+ if (applicationInfo.isSystemApp()) {
+ systemInputMethods.add(inputMethodInfo.getPackageName());
+ }
+ }
+ return systemInputMethods;
+ }
+
+ private Set<String> getRequiredApps(String provisioningAction, String dpcPackageName) {
+ final Set<String> requiredApps = new ArraySet<>();
+ requiredApps.addAll(getRequiredAppsSet(provisioningAction));
+ requiredApps.addAll(getVendorRequiredAppsSet(provisioningAction));
+ requiredApps.add(dpcPackageName);
+ return requiredApps;
+ }
+
+ private Set<String> getDisallowedApps(String provisioningAction) {
+ final Set<String> disallowedApps = new ArraySet<>();
+ disallowedApps.addAll(getDisallowedAppsSet(provisioningAction));
+ disallowedApps.addAll(getVendorDisallowedAppsSet(provisioningAction));
+ return disallowedApps;
+ }
+
+ private static IInputMethodManager getIInputMethodManager() {
+ final IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
+ return IInputMethodManager.Stub.asInterface(b);
+ }
+
+ private Set<String> getRequiredAppsSet(String provisioningAction) {
+ final int resId;
+ switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER:
+ resId = R.array.required_apps_managed_user;
+ break;
+ case ACTION_PROVISION_MANAGED_PROFILE:
+ resId = R.array.required_apps_managed_profile;
+ break;
+ case ACTION_PROVISION_MANAGED_DEVICE:
+ resId = R.array.required_apps_managed_device;
+ break;
+ default:
+ throw new IllegalArgumentException("Provisioning type "
+ + provisioningAction + " not supported.");
+ }
+ return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ }
+
+ private Set<String> getDisallowedAppsSet(String provisioningAction) {
+ final int resId;
+ switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER:
+ resId = R.array.disallowed_apps_managed_user;
+ break;
+ case ACTION_PROVISION_MANAGED_PROFILE:
+ resId = R.array.disallowed_apps_managed_profile;
+ break;
+ case ACTION_PROVISION_MANAGED_DEVICE:
+ resId = R.array.disallowed_apps_managed_device;
+ break;
+ default:
+ throw new IllegalArgumentException("Provisioning type "
+ + provisioningAction + " not supported.");
+ }
+ return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ }
+
+ private Set<String> getVendorRequiredAppsSet(String provisioningAction) {
+ final int resId;
+ switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER:
+ resId = R.array.vendor_required_apps_managed_user;
+ break;
+ case ACTION_PROVISION_MANAGED_PROFILE:
+ resId = R.array.vendor_required_apps_managed_profile;
+ break;
+ case ACTION_PROVISION_MANAGED_DEVICE:
+ resId = R.array.vendor_required_apps_managed_device;
+ break;
+ default:
+ throw new IllegalArgumentException("Provisioning type "
+ + provisioningAction + " not supported.");
+ }
+ return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ }
+
+ private Set<String> getVendorDisallowedAppsSet(String provisioningAction) {
+ final int resId;
+ switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER:
+ resId = R.array.vendor_disallowed_apps_managed_user;
+ break;
+ case ACTION_PROVISION_MANAGED_PROFILE:
+ resId = R.array.vendor_disallowed_apps_managed_profile;
+ break;
+ case ACTION_PROVISION_MANAGED_DEVICE:
+ resId = R.array.vendor_disallowed_apps_managed_device;
+ break;
+ default:
+ throw new IllegalArgumentException("Provisioning type "
+ + provisioningAction + " not supported.");
+ }
+ return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ }
+}
diff --git a/com/android/server/devicepolicy/Owners.java b/com/android/server/devicepolicy/Owners.java
index a5500dd7..9042a8d8 100644
--- a/com/android/server/devicepolicy/Owners.java
+++ b/com/android/server/devicepolicy/Owners.java
@@ -277,6 +277,17 @@ class Owners {
}
}
+ void transferProfileOwner(ComponentName target, int userId) {
+ synchronized (mLock) {
+ final OwnerInfo ownerInfo = mProfileOwners.get(userId);
+ final OwnerInfo newOwnerInfo = new OwnerInfo(target.getPackageName(), target,
+ ownerInfo.userRestrictionsMigrated, ownerInfo.remoteBugreportUri,
+ ownerInfo.remoteBugreportHash);
+ mProfileOwners.put(userId, newOwnerInfo);
+ pushToPackageManagerLocked();
+ }
+ }
+
ComponentName getProfileOwnerComponent(int userId) {
synchronized (mLock) {
OwnerInfo profileOwner = mProfileOwners.get(userId);
diff --git a/com/android/server/devicepolicy/PasswordBlacklist.java b/com/android/server/devicepolicy/PasswordBlacklist.java
new file mode 100644
index 00000000..6a9b53a0
--- /dev/null
+++ b/com/android/server/devicepolicy/PasswordBlacklist.java
@@ -0,0 +1,165 @@
+/*
+ * 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 com.android.server.devicepolicy;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Manages the blacklisted passwords.
+ *
+ * This caller must ensure synchronized access.
+ */
+public class PasswordBlacklist {
+ private static final String TAG = "PasswordBlacklist";
+
+ private final AtomicFile mFile;
+
+ /**
+ * Create an object to manage the password blacklist.
+ *
+ * This is a lightweight operation to prepare variables but not perform any IO.
+ */
+ public PasswordBlacklist(File file) {
+ mFile = new AtomicFile(file);
+ }
+
+ /**
+ * Atomically replace the blacklist.
+ *
+ * Pass {@code null} for an empty list.
+ */
+ public boolean savePasswordBlacklist(@NonNull String name, @NonNull List<String> blacklist) {
+ FileOutputStream fos = null;
+ try {
+ fos = mFile.startWrite();
+ final DataOutputStream out = buildStreamForWriting(fos);
+ final Header header = new Header(Header.VERSION_1, name, blacklist.size());
+ header.write(out);
+ final int blacklistSize = blacklist.size();
+ for (int i = 0; i < blacklistSize; ++i) {
+ out.writeUTF(blacklist.get(i));
+ }
+ out.flush();
+ mFile.finishWrite(fos);
+ return true;
+ } catch (IOException e) {
+ mFile.failWrite(fos);
+ return false;
+ }
+ }
+
+ /** @return the name of the blacklist or {@code null} if none set. */
+ public String getName() {
+ try (DataInputStream in = openForReading()) {
+ return Header.read(in).mName;
+ } catch (IOException e) {
+ Slog.wtf(TAG, "Failed to read blacklist file", e);
+ }
+ return null;
+ }
+
+ /** @return the number of blacklisted passwords. */
+ public int getSize() {
+ final int blacklistSize;
+ try (DataInputStream in = openForReading()) {
+ return Header.read(in).mSize;
+ } catch (IOException e) {
+ Slog.wtf(TAG, "Failed to read blacklist file", e);
+ }
+ return 0;
+ }
+
+ /** @return whether the password matches an blacklisted item. */
+ public boolean isPasswordBlacklisted(@NonNull String password) {
+ final int blacklistSize;
+ try (DataInputStream in = openForReading()) {
+ final Header header = Header.read(in);
+ for (int i = 0; i < header.mSize; ++i) {
+ if (in.readUTF().equals(password)) {
+ return true;
+ }
+ }
+ } catch (IOException e) {
+ Slog.wtf(TAG, "Failed to read blacklist file", e);
+ // Fail safe and block all passwords. Setting a new blacklist should resolve this
+ // problem which can be identified by examining the log.
+ return true;
+ }
+ return false;
+ }
+
+ /** Delete the blacklist completely from disk. */
+ public void delete() {
+ mFile.delete();
+ }
+
+ /** Get the file the blacklist is stored in. */
+ public File getFile() {
+ return mFile.getBaseFile();
+ }
+
+ private DataOutputStream buildStreamForWriting(FileOutputStream fos) {
+ return new DataOutputStream(new BufferedOutputStream(fos));
+ }
+
+ private DataInputStream openForReading() throws IOException {
+ return new DataInputStream(new BufferedInputStream(mFile.openRead()));
+ }
+
+ /**
+ * Helper to read and write the header of the blacklist file.
+ */
+ private static class Header {
+ static final int VERSION_1 = 1;
+
+ final int mVersion; // File format version
+ final String mName;
+ final int mSize;
+
+ Header(int version, String name, int size) {
+ mVersion = version;
+ mName = name;
+ mSize = size;
+ }
+
+ void write(DataOutputStream out) throws IOException {
+ out.writeInt(mVersion);
+ out.writeUTF(mName);
+ out.writeInt(mSize);
+ }
+
+ static Header read(DataInputStream in) throws IOException {
+ final int version = in.readInt();
+ final String name = in.readUTF();
+ final int size = in.readInt();
+ return new Header(version, name, size);
+ }
+ }
+}
diff --git a/com/android/server/devicepolicy/SecurityLogMonitor.java b/com/android/server/devicepolicy/SecurityLogMonitor.java
index 5c3a37ae..a9fd8e53 100644
--- a/com/android/server/devicepolicy/SecurityLogMonitor.java
+++ b/com/android/server/devicepolicy/SecurityLogMonitor.java
@@ -19,11 +19,13 @@ package com.android.server.devicepolicy;
import android.app.admin.DeviceAdminReceiver;
import android.app.admin.SecurityLog;
import android.app.admin.SecurityLog.SecurityEvent;
+import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.ArrayList;
@@ -32,8 +34,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
-import android.os.Process;
-
/**
* A class managing access to the security logs. It maintains an internal buffer of pending
* logs to be retrieved by the device owner. The logs are retrieved from the logd daemon via
@@ -48,7 +48,13 @@ class SecurityLogMonitor implements Runnable {
private final Lock mLock = new ReentrantLock();
SecurityLogMonitor(DevicePolicyManagerService service) {
- mService = service;
+ this(service, 0 /* id */);
+ }
+
+ @VisibleForTesting
+ SecurityLogMonitor(DevicePolicyManagerService service, long id) {
+ this.mService = service;
+ this.mId = id;
}
private static final boolean DEBUG = false; // STOPSHIP if true.
@@ -58,7 +64,7 @@ class SecurityLogMonitor implements Runnable {
* it should be less than 100 bytes), setting 1024 entries as the threshold to notify Device
* Owner.
*/
- private static final int BUFFER_ENTRIES_NOTIFICATION_LEVEL = 1024;
+ @VisibleForTesting static final int BUFFER_ENTRIES_NOTIFICATION_LEVEL = 1024;
/**
* The maximum number of entries we should store before dropping earlier logs, to limit the
* memory usage.
@@ -87,6 +93,8 @@ class SecurityLogMonitor implements Runnable {
@GuardedBy("mLock")
private ArrayList<SecurityEvent> mPendingLogs = new ArrayList<>();
@GuardedBy("mLock")
+ private long mId;
+ @GuardedBy("mLock")
private boolean mAllowedToRetrieve = false;
/**
@@ -112,6 +120,7 @@ class SecurityLogMonitor implements Runnable {
try {
if (mMonitorThread == null) {
mPendingLogs = new ArrayList<>();
+ mId = 0;
mAllowedToRetrieve = false;
mNextAllowedRetrievalTimeMillis = -1;
mPaused = false;
@@ -137,6 +146,7 @@ class SecurityLogMonitor implements Runnable {
}
// Reset state and clear buffer
mPendingLogs = new ArrayList<>();
+ mId = 0;
mAllowedToRetrieve = false;
mNextAllowedRetrievalTimeMillis = -1;
mPaused = false;
@@ -305,6 +315,7 @@ class SecurityLogMonitor implements Runnable {
if (lastNanos > currentNanos) {
// New event older than the last we've seen so far, must be due to reordering.
if (DEBUG) Slog.d(TAG, "New event in the overlap: " + currentNanos);
+ assignLogId(curEvent);
mPendingLogs.add(curEvent);
curPos++;
} else if (lastNanos < currentNanos) {
@@ -317,6 +328,7 @@ class SecurityLogMonitor implements Runnable {
if (DEBUG) Slog.d(TAG, "Skipped dup event with timestamp: " + lastNanos);
} else {
// Wow, what a coincidence, or probably the clock is too coarse.
+ assignLogId(curEvent);
mPendingLogs.add(curEvent);
if (DEBUG) Slog.d(TAG, "Event timestamp collision: " + lastNanos);
}
@@ -324,8 +336,13 @@ class SecurityLogMonitor implements Runnable {
curPos++;
}
}
+ // Assign an id to the new logs, after the overlap with mLastEvents.
+ List<SecurityEvent> idLogs = newLogs.subList(curPos, newLogs.size());
+ for (SecurityEvent event : idLogs) {
+ assignLogId(event);
+ }
// Save the rest of the new batch.
- mPendingLogs.addAll(newLogs.subList(curPos, newLogs.size()));
+ mPendingLogs.addAll(idLogs);
if (mPendingLogs.size() > BUFFER_ENTRIES_MAXIMUM_LEVEL) {
// Truncate buffer down to half of BUFFER_ENTRIES_MAXIMUM_LEVEL.
@@ -334,7 +351,20 @@ class SecurityLogMonitor implements Runnable {
mPendingLogs.size()));
Slog.i(TAG, "Pending logs buffer full. Discarding old logs.");
}
- if (DEBUG) Slog.d(TAG, mPendingLogs.size() + " pending events in the buffer after merging");
+ if (DEBUG) Slog.d(TAG, mPendingLogs.size() + " pending events in the buffer after merging,"
+ + " with ids " + mPendingLogs.get(0).getId()
+ + " to " + mPendingLogs.get(mPendingLogs.size() - 1).getId());
+ }
+
+ @GuardedBy("mLock")
+ private void assignLogId(SecurityEvent event) {
+ event.setId(mId);
+ if (mId == Long.MAX_VALUE) {
+ Slog.i(TAG, "Reached maximum id value; wrapping around.");
+ mId = 0;
+ } else {
+ mId++;
+ }
}
@Override
diff --git a/com/android/server/display/AutomaticBrightnessController.java b/com/android/server/display/AutomaticBrightnessController.java
index 9a6e6094..6c5bfc79 100644
--- a/com/android/server/display/AutomaticBrightnessController.java
+++ b/com/android/server/display/AutomaticBrightnessController.java
@@ -24,6 +24,7 @@ import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.display.BrightnessConfiguration;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -34,7 +35,6 @@ import android.text.format.DateUtils;
import android.util.EventLog;
import android.util.MathUtils;
import android.util.Slog;
-import android.util.Spline;
import android.util.TimeUtils;
import java.io.PrintWriter;
@@ -76,9 +76,8 @@ class AutomaticBrightnessController {
// The light sensor, or null if not available or needed.
private final Sensor mLightSensor;
- // The auto-brightness spline adjustment.
- // The brightness values have been scaled to a range of 0..1.
- private final Spline mScreenAutoBrightnessSpline;
+ // The mapper to translate ambient lux to screen brightness in the range [0, 1.0].
+ private final BrightnessMappingStrategy mBrightnessMapper;
// The minimum and maximum screen brightnesses.
private final int mScreenBrightnessRangeMinimum;
@@ -186,7 +185,7 @@ class AutomaticBrightnessController {
private float mBrightnessAdjustmentSampleOldGamma;
public AutomaticBrightnessController(Callbacks callbacks, Looper looper,
- SensorManager sensorManager, Spline autoBrightnessSpline, int lightSensorWarmUpTime,
+ SensorManager sensorManager, BrightnessMappingStrategy mapper, int lightSensorWarmUpTime,
int brightnessMin, int brightnessMax, float dozeScaleFactor,
int lightSensorRate, int initialLightSensorRate, long brighteningLightDebounceConfig,
long darkeningLightDebounceConfig, boolean resetAmbientLuxAfterWarmUpConfig,
@@ -194,7 +193,7 @@ class AutomaticBrightnessController {
HysteresisLevels dynamicHysteresis) {
mCallbacks = callbacks;
mSensorManager = sensorManager;
- mScreenAutoBrightnessSpline = autoBrightnessSpline;
+ mBrightnessMapper = mapper;
mScreenBrightnessRangeMinimum = brightnessMin;
mScreenBrightnessRangeMaximum = brightnessMax;
mLightSensorWarmUpTimeConfig = lightSensorWarmUpTime;
@@ -228,15 +227,16 @@ class AutomaticBrightnessController {
return mScreenAutoBrightness;
}
- public void configure(boolean enable, float adjustment, boolean dozing,
- boolean userInitiatedChange) {
+ public void configure(boolean enable, @Nullable BrightnessConfiguration configuration,
+ float adjustment, boolean dozing, boolean userInitiatedChange) {
// While dozing, the application processor may be suspended which will prevent us from
// receiving new information from the light sensor. On some devices, we may be able to
// switch to a wake-up light sensor instead but for now we will simply disable the sensor
// and hold onto the last computed screen auto brightness. We save the dozing flag for
// debugging purposes.
mDozing = dozing;
- boolean changed = setLightSensorEnabled(enable && !dozing);
+ boolean changed = setBrightnessConfiguration(configuration);
+ changed |= setLightSensorEnabled(enable && !dozing);
if (enable && !dozing && userInitiatedChange) {
prepareBrightnessAdjustmentSample();
}
@@ -246,10 +246,13 @@ class AutomaticBrightnessController {
}
}
+ public boolean setBrightnessConfiguration(BrightnessConfiguration configuration) {
+ return mBrightnessMapper.setBrightnessConfiguration(configuration);
+ }
+
public void dump(PrintWriter pw) {
pw.println();
pw.println("Automatic Brightness Controller Configuration:");
- pw.println(" mScreenAutoBrightnessSpline=" + mScreenAutoBrightnessSpline);
pw.println(" mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum);
pw.println(" mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum);
pw.println(" mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);
@@ -274,9 +277,13 @@ class AutomaticBrightnessController {
mInitialHorizonAmbientLightRingBuffer);
pw.println(" mScreenAutoBrightness=" + mScreenAutoBrightness);
pw.println(" mScreenAutoBrightnessAdjustment=" + mScreenAutoBrightnessAdjustment);
- pw.println(" mScreenAutoBrightnessAdjustmentMaxGamma=" + mScreenAutoBrightnessAdjustmentMaxGamma);
+ pw.println(" mScreenAutoBrightnessAdjustmentMaxGamma="
+ + mScreenAutoBrightnessAdjustmentMaxGamma);
pw.println(" mLastScreenAutoBrightnessGamma=" + mLastScreenAutoBrightnessGamma);
pw.println(" mDozing=" + mDozing);
+
+ pw.println();
+ mBrightnessMapper.dump(pw);
}
private boolean setLightSensorEnabled(boolean enable) {
@@ -533,7 +540,7 @@ class AutomaticBrightnessController {
return;
}
- float value = mScreenAutoBrightnessSpline.interpolate(mAmbientLux);
+ float value = mBrightnessMapper.getBrightness(mAmbientLux);
float gamma = 1.0f;
if (USE_SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT
diff --git a/com/android/server/display/BrightnessMappingStrategy.java b/com/android/server/display/BrightnessMappingStrategy.java
new file mode 100644
index 00000000..3b9d40fa
--- /dev/null
+++ b/com/android/server/display/BrightnessMappingStrategy.java
@@ -0,0 +1,289 @@
+/*
+ * 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 com.android.server.display;
+
+import android.annotation.Nullable;
+import android.hardware.display.BrightnessConfiguration;
+import android.os.PowerManager;
+import android.util.MathUtils;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.Spline;
+
+import com.android.internal.util.Preconditions;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+/**
+ * A utility to map from an ambient brightness to a display's "backlight" brightness based on the
+ * available display information and brightness configuration.
+ *
+ * Note that without a mapping from the nits to a display backlight level, any
+ * {@link BrightnessConfiguration}s that are set are just ignored.
+ */
+public abstract class BrightnessMappingStrategy {
+ private static final String TAG = "BrightnessMappingStrategy";
+ private static final boolean DEBUG = false;
+
+ @Nullable
+ public static BrightnessMappingStrategy create(
+ float[] luxLevels, int[] brightnessLevelsBacklight, float[] brightnessLevelsNits,
+ float[] nitsRange, int[] backlightRange) {
+ if (isValidMapping(nitsRange, backlightRange)
+ && isValidMapping(luxLevels, brightnessLevelsNits)) {
+ BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+ builder.setCurve(luxLevels, brightnessLevelsNits);
+ return new PhysicalMappingStrategy(builder.build(), nitsRange, backlightRange);
+ } else if (isValidMapping(luxLevels, brightnessLevelsBacklight)) {
+ return new SimpleMappingStrategy(luxLevels, brightnessLevelsBacklight);
+ } else {
+ return null;
+ }
+ }
+
+ private static boolean isValidMapping(float[] x, float[] y) {
+ if (x == null || y == null || x.length == 0 || y.length == 0) {
+ return false;
+ }
+ if (x.length != y.length) {
+ return false;
+ }
+ final int N = x.length;
+ float prevX = x[0];
+ float prevY = y[0];
+ if (prevX < 0 || prevY < 0 || Float.isNaN(prevX) || Float.isNaN(prevY)) {
+ return false;
+ }
+ for (int i = 1; i < N; i++) {
+ if (prevX >= x[i] || prevY > y[i]) {
+ return false;
+ }
+ if (Float.isNaN(x[i]) || Float.isNaN(y[i])) {
+ return false;
+ }
+ prevX = x[i];
+ prevY = y[i];
+ }
+ return true;
+ }
+
+ private static boolean isValidMapping(float[] x, int[] y) {
+ if (x == null || y == null || x.length == 0 || y.length == 0) {
+ return false;
+ }
+ if (x.length != y.length) {
+ return false;
+ }
+ final int N = x.length;
+ float prevX = x[0];
+ int prevY = y[0];
+ if (prevX < 0 || prevY < 0 || Float.isNaN(prevX)) {
+ return false;
+ }
+ for (int i = 1; i < N; i++) {
+ if (prevX >= x[i] || prevY > y[i]) {
+ return false;
+ }
+ if (Float.isNaN(x[i])) {
+ return false;
+ }
+ prevX = x[i];
+ prevY = y[i];
+ }
+ return true;
+ }
+
+ /**
+ * Sets the {@link BrightnessConfiguration}.
+ *
+ * @param config The new configuration. If {@code null} is passed, the default configuration is
+ * used.
+ * @return Whether the brightness configuration has changed.
+ */
+ public abstract boolean setBrightnessConfiguration(@Nullable BrightnessConfiguration config);
+
+ /**
+ * Returns the desired brightness of the display based on the current ambient lux.
+ *
+ * The returned brightness will be in the range [0, 1.0], where 1.0 is the display at max
+ * brightness and 0 is the display at minimum brightness.
+ *
+ * @param lux The current ambient brightness in lux.
+ * @return The desired brightness of the display compressed to the range [0, 1.0].
+ */
+ public abstract float getBrightness(float lux);
+
+ public abstract void dump(PrintWriter pw);
+
+ private static float normalizeAbsoluteBrightness(int brightness) {
+ brightness = MathUtils.constrain(brightness,
+ PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON);
+ return (float) brightness / PowerManager.BRIGHTNESS_ON;
+ }
+
+
+ /**
+ * A {@link BrightnessMappingStrategy} that maps from ambient room brightness directly to the
+ * backlight of the display.
+ *
+ * Since we don't have information about the display's physical brightness, any brightness
+ * configurations that are set are just ignored.
+ */
+ private static class SimpleMappingStrategy extends BrightnessMappingStrategy {
+ private final Spline mSpline;
+
+ public SimpleMappingStrategy(float[] lux, int[] brightness) {
+ Preconditions.checkArgument(lux.length != 0 && brightness.length != 0,
+ "Lux and brightness arrays must not be empty!");
+ Preconditions.checkArgument(lux.length == brightness.length,
+ "Lux and brightness arrays must be the same length!");
+ Preconditions.checkArrayElementsInRange(lux, 0, Float.MAX_VALUE, "lux");
+ Preconditions.checkArrayElementsInRange(brightness,
+ 0, Integer.MAX_VALUE, "brightness");
+
+ final int N = brightness.length;
+ float[] x = new float[N];
+ float[] y = new float[N];
+ for (int i = 0; i < N; i++) {
+ x[i] = lux[i];
+ y[i] = normalizeAbsoluteBrightness(brightness[i]);
+ }
+
+ mSpline = Spline.createSpline(x, y);
+ if (DEBUG) {
+ Slog.d(TAG, "Auto-brightness spline: " + mSpline);
+ for (float v = 1f; v < lux[lux.length - 1] * 1.25f; v *= 1.25f) {
+ Slog.d(TAG, String.format(" %7.1f: %7.1f", v, mSpline.interpolate(v)));
+ }
+ }
+ }
+
+ @Override
+ public boolean setBrightnessConfiguration(@Nullable BrightnessConfiguration config) {
+ Slog.e(TAG,
+ "setBrightnessConfiguration called on device without display information.");
+ return false;
+ }
+
+ @Override
+ public float getBrightness(float lux) {
+ return mSpline.interpolate(lux);
+ }
+
+ @Override
+ public void dump(PrintWriter pw) {
+ pw.println("SimpleMappingStrategy");
+ pw.println(" mSpline=" + mSpline);
+ }
+ }
+
+ /** A {@link BrightnessMappingStrategy} that maps from ambient room brightness to the physical
+ * range of the display, rather than to the range of the backlight control (typically 0-255).
+ *
+ * By mapping through the physical brightness, the curve becomes portable across devices and
+ * gives us more resolution in the resulting mapping.
+ */
+ @VisibleForTesting
+ static class PhysicalMappingStrategy extends BrightnessMappingStrategy {
+ // The current brightness configuration.
+ private BrightnessConfiguration mConfig;
+
+ // A spline mapping from the current ambient light in lux to the desired display brightness
+ // in nits.
+ private Spline mBrightnessSpline;
+
+ // A spline mapping from nits to the corresponding backlight value, normalized to the range
+ // [0, 1.0].
+ private final Spline mBacklightSpline;
+
+ // The default brightness configuration.
+ private final BrightnessConfiguration mDefaultConfig;
+
+ public PhysicalMappingStrategy(BrightnessConfiguration config,
+ float[] nits, int[] backlight) {
+ Preconditions.checkArgument(nits.length != 0 && backlight.length != 0,
+ "Nits and backlight arrays must not be empty!");
+ Preconditions.checkArgument(nits.length == backlight.length,
+ "Nits and backlight arrays must be the same length!");
+ Preconditions.checkNotNull(config);
+ Preconditions.checkArrayElementsInRange(nits, 0, Float.MAX_VALUE, "nits");
+ Preconditions.checkArrayElementsInRange(backlight,
+ PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON, "backlight");
+
+ // Setup the backlight spline
+ final int N = nits.length;
+ float[] x = new float[N];
+ float[] y = new float[N];
+ for (int i = 0; i < N; i++) {
+ x[i] = nits[i];
+ y[i] = normalizeAbsoluteBrightness(backlight[i]);
+ }
+
+ mBacklightSpline = Spline.createSpline(x, y);
+ if (DEBUG) {
+ Slog.d(TAG, "Backlight spline: " + mBacklightSpline);
+ for (float v = 1f; v < nits[nits.length - 1] * 1.25f; v *= 1.25f) {
+ Slog.d(TAG, String.format(
+ " %7.1f: %7.1f", v, mBacklightSpline.interpolate(v)));
+ }
+ }
+
+ mDefaultConfig = config;
+ setBrightnessConfiguration(config);
+ }
+
+ @Override
+ public boolean setBrightnessConfiguration(@Nullable BrightnessConfiguration config) {
+ if (config == null) {
+ config = mDefaultConfig;
+ }
+ if (config.equals(mConfig)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Tried to set an identical brightness config, ignoring");
+ }
+ return false;
+ }
+
+ Pair<float[], float[]> curve = config.getCurve();
+ mBrightnessSpline = Spline.createSpline(curve.first /*lux*/, curve.second /*nits*/);
+ if (DEBUG) {
+ Slog.d(TAG, "Brightness spline: " + mBrightnessSpline);
+ final float[] lux = curve.first;
+ for (float v = 1f; v < lux[lux.length - 1] * 1.25f; v *= 1.25f) {
+ Slog.d(TAG, String.format(
+ " %7.1f: %7.1f", v, mBrightnessSpline.interpolate(v)));
+ }
+ }
+ mConfig = config;
+ return true;
+ }
+
+ @Override
+ public float getBrightness(float lux) {
+ return mBacklightSpline.interpolate(mBrightnessSpline.interpolate(lux));
+ }
+
+ @Override
+ public void dump(PrintWriter pw) {
+ pw.println("PhysicalMappingStrategy");
+ pw.println(" mConfig=" + mConfig);
+ pw.println(" mBrightnessSpline=" + mBrightnessSpline);
+ pw.println(" mBacklightSpline=" + mBacklightSpline);
+ }
+ }
+}
diff --git a/com/android/server/display/BrightnessTracker.java b/com/android/server/display/BrightnessTracker.java
index 2c6fe948..42247f94 100644
--- a/com/android/server/display/BrightnessTracker.java
+++ b/com/android/server/display/BrightnessTracker.java
@@ -196,9 +196,10 @@ public class BrightnessTracker {
/**
* @param userId userId to fetch data for.
+ * @param includePackage if false we will null out BrightnessChangeEvent.packageName
* @return List of recent {@link BrightnessChangeEvent}s
*/
- public ParceledListSlice<BrightnessChangeEvent> getEvents(int userId) {
+ public ParceledListSlice<BrightnessChangeEvent> getEvents(int userId, boolean includePackage) {
// TODO include apps from any managed profiles in the brightness information.
BrightnessChangeEvent[] events;
synchronized (mEventsLock) {
@@ -207,7 +208,13 @@ public class BrightnessTracker {
ArrayList<BrightnessChangeEvent> out = new ArrayList<>(events.length);
for (int i = 0; i < events.length; ++i) {
if (events[i].userId == userId) {
- out.add(events[i]);
+ if (includePackage) {
+ out.add(events[i]);
+ } else {
+ BrightnessChangeEvent event = new BrightnessChangeEvent((events[i]));
+ event.packageName = null;
+ out.add(event);
+ }
}
}
return new ParceledListSlice<>(out);
@@ -493,13 +500,30 @@ public class BrightnessTracker {
}
public void dump(PrintWriter pw) {
- synchronized (mEventsLock) {
- pw.println("BrightnessTracker state:");
- pw.println(" mEvents.size=" + mEvents.size());
- pw.println(" mEventsDirty=" + mEventsDirty);
- }
+ pw.println("BrightnessTracker state:");
synchronized (mDataCollectionLock) {
pw.println(" mLastSensorReadings.size=" + mLastSensorReadings.size());
+ if (!mLastSensorReadings.isEmpty()) {
+ pw.println(" mLastSensorReadings time span "
+ + mLastSensorReadings.peekFirst().timestamp + "->"
+ + mLastSensorReadings.peekLast().timestamp);
+ }
+ }
+ synchronized (mEventsLock) {
+ pw.println(" mEventsDirty=" + mEventsDirty);
+ pw.println(" mEvents.size=" + mEvents.size());
+ BrightnessChangeEvent[] events = mEvents.toArray();
+ for (int i = 0; i < events.length; ++i) {
+ pw.print(" " + events[i].timeStamp + ", " + events[i].userId);
+ pw.print(", " + events[i].lastBrightness + "->" + events[i].brightness + ", {");
+ for (int j = 0; j < events[i].luxValues.length; ++j){
+ if (j != 0) {
+ pw.print(", ");
+ }
+ pw.print("(" + events[i].luxValues[j] + "," + events[i].luxTimestamps[j] + ")");
+ }
+ pw.println("}");
+ }
}
}
diff --git a/com/android/server/display/ColorDisplayService.java b/com/android/server/display/ColorDisplayService.java
index af8ecadd..b3d309dd 100644
--- a/com/android/server/display/ColorDisplayService.java
+++ b/com/android/server/display/ColorDisplayService.java
@@ -33,11 +33,8 @@ import android.net.Uri;
import android.opengl.Matrix;
import android.os.Handler;
import android.os.Looper;
-import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings.Secure;
-import android.service.vr.IVrManager;
-import android.service.vr.IVrStateCallbacks;
import android.util.MathUtils;
import android.util.Slog;
import android.view.animation.AnimationUtils;
@@ -51,7 +48,6 @@ import com.android.server.twilight.TwilightState;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
-import java.util.concurrent.atomic.AtomicBoolean;
import com.android.internal.R;
@@ -84,32 +80,6 @@ public final class ColorDisplayService extends SystemService
private static final ColorMatrixEvaluator COLOR_MATRIX_EVALUATOR = new ColorMatrixEvaluator();
private final Handler mHandler;
- private final AtomicBoolean mIgnoreAllColorMatrixChanges = new AtomicBoolean();
- private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
- @Override
- public void onVrStateChanged(final boolean enabled) {
- // Turn off all night mode display stuff while device is in VR mode.
- mIgnoreAllColorMatrixChanges.set(enabled);
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- // Cancel in-progress animations
- if (mColorMatrixAnimator != null) {
- mColorMatrixAnimator.cancel();
- }
-
- final DisplayTransformManager dtm =
- getLocalService(DisplayTransformManager.class);
- if (enabled) {
- dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, MATRIX_IDENTITY);
- } else if (mController != null && mController.isActivated()) {
- setMatrix(mController.getColorTemperature(), mMatrixNight);
- dtm.setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, mMatrixNight);
- }
- }
- });
- }
- };
private float[] mMatrixNight = new float[16];
@@ -136,18 +106,6 @@ public final class ColorDisplayService extends SystemService
@Override
public void onBootPhase(int phase) {
- if (phase >= PHASE_SYSTEM_SERVICES_READY) {
- final IVrManager vrManager = IVrManager.Stub.asInterface(
- getBinderService(Context.VR_SERVICE));
- if (vrManager != null) {
- try {
- vrManager.registerListener(mVrStateCallbacks);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to register VR mode state listener: " + e);
- }
- }
- }
-
if (phase >= PHASE_BOOT_COMPLETED) {
mBootCompleted = true;
@@ -231,7 +189,7 @@ public final class ColorDisplayService extends SystemService
mController = new ColorDisplayController(getContext(), mCurrentUser);
mController.setListener(this);
- setCoefficientMatrix(getContext());
+ setCoefficientMatrix(getContext(), DisplayTransformManager.isNativeModeEnabled());
// Prepare color transformation matrix.
setMatrix(mController.getColorTemperature(), mMatrixNight);
@@ -329,17 +287,24 @@ public final class ColorDisplayService extends SystemService
}
@Override
- public void onDisplayColorModeChanged(int colorMode) {
- final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class);
- dtm.setColorMode(colorMode);
+ public void onDisplayColorModeChanged(int mode) {
+ // Cancel the night display tint animator if it's running.
+ if (mColorMatrixAnimator != null) {
+ mColorMatrixAnimator.cancel();
+ }
- setCoefficientMatrix(getContext());
+ setCoefficientMatrix(getContext(), mode == ColorDisplayController.COLOR_MODE_SATURATED);
setMatrix(mController.getColorTemperature(), mMatrixNight);
- applyTint(true);
+
+ final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class);
+ dtm.setColorMode(mode, mIsActivated ? mMatrixNight : MATRIX_IDENTITY);
}
- private void setCoefficientMatrix(Context context) {
- final boolean isNative = DisplayTransformManager.isNativeModeEnabled();
+ /**
+ * Set coefficients based on native mode. Use DisplayTransformManager#isNativeModeEnabled while
+ * setting is stable; when setting is changing, pass native mode selection directly.
+ */
+ private void setCoefficientMatrix(Context context, boolean isNative) {
final String[] coefficients = context.getResources().getStringArray(isNative
? R.array.config_nightDisplayColorTemperatureCoefficientsNative
: R.array.config_nightDisplayColorTemperatureCoefficients);
@@ -359,11 +324,6 @@ public final class ColorDisplayService extends SystemService
mColorMatrixAnimator.cancel();
}
- // Don't do any color matrix change animations if we are ignoring them anyway.
- if (mIgnoreAllColorMatrixChanges.get()) {
- return;
- }
-
final DisplayTransformManager dtm = getLocalService(DisplayTransformManager.class);
final float[] from = dtm.getColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY);
final float[] to = mIsActivated ? mMatrixNight : MATRIX_IDENTITY;
diff --git a/com/android/server/display/ColorFade.java b/com/android/server/display/ColorFade.java
index c2167eb8..85686ae9 100644
--- a/com/android/server/display/ColorFade.java
+++ b/com/android/server/display/ColorFade.java
@@ -99,7 +99,7 @@ final class ColorFade {
private final float mProjMatrix[] = new float[16];
private final int[] mGLBuffers = new int[2];
private int mTexCoordLoc, mVertexLoc, mTexUnitLoc, mProjMatrixLoc, mTexMatrixLoc;
- private int mOpacityLoc, mScaleLoc, mGammaLoc, mSaturationLoc;
+ private int mOpacityLoc, mGammaLoc, mSaturationLoc;
private int mProgram;
// Vertex and corresponding texture coordinates.
@@ -246,7 +246,6 @@ final class ColorFade {
mOpacityLoc = GLES20.glGetUniformLocation(mProgram, "opacity");
mGammaLoc = GLES20.glGetUniformLocation(mProgram, "gamma");
mSaturationLoc = GLES20.glGetUniformLocation(mProgram, "saturation");
- mScaleLoc = GLES20.glGetUniformLocation(mProgram, "scale");
mTexUnitLoc = GLES20.glGetUniformLocation(mProgram, "texUnit");
GLES20.glUseProgram(mProgram);
@@ -395,9 +394,8 @@ final class ColorFade {
double sign = cos < 0 ? -1 : 1;
float opacity = (float) -Math.pow(one_minus_level, 2) + 1;
float saturation = (float) Math.pow(level, 4);
- float scale = (float) ((-Math.pow(one_minus_level, 2) + 1) * 0.1d + 0.9d);
float gamma = (float) ((0.5d * sign * Math.pow(cos, 2) + 0.5d) * 0.9d + 0.1d);
- drawFaded(opacity, 1.f / gamma, saturation, scale);
+ drawFaded(opacity, 1.f / gamma, saturation);
if (checkGlErrors("drawFrame")) {
return false;
}
@@ -409,10 +407,10 @@ final class ColorFade {
return showSurface(1.0f);
}
- private void drawFaded(float opacity, float gamma, float saturation, float scale) {
+ private void drawFaded(float opacity, float gamma, float saturation) {
if (DEBUG) {
Slog.d(TAG, "drawFaded: opacity=" + opacity + ", gamma=" + gamma +
- ", saturation=" + saturation + ", scale=" + scale);
+ ", saturation=" + saturation);
}
// Use shaders
GLES20.glUseProgram(mProgram);
@@ -423,7 +421,6 @@ final class ColorFade {
GLES20.glUniform1f(mOpacityLoc, opacity);
GLES20.glUniform1f(mGammaLoc, gamma);
GLES20.glUniform1f(mSaturationLoc, saturation);
- GLES20.glUniform1f(mScaleLoc, scale);
// Use textures
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
diff --git a/com/android/server/display/DisplayManagerService.java b/com/android/server/display/DisplayManagerService.java
index 379aaadc..9b97934c 100644
--- a/com/android/server/display/DisplayManagerService.java
+++ b/com/android/server/display/DisplayManagerService.java
@@ -29,6 +29,8 @@ import com.android.internal.util.IndentingPrintWriter;
import android.Manifest;
import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
@@ -36,6 +38,7 @@ import android.content.res.Resources;
import android.graphics.Point;
import android.hardware.SensorManager;
import android.hardware.display.BrightnessChangeEvent;
+import android.hardware.display.BrightnessConfiguration;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.display.DisplayViewport;
@@ -61,6 +64,7 @@ import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
+import android.os.UserManager;
import android.text.TextUtils;
import android.util.IntArray;
import android.util.Slog;
@@ -69,12 +73,14 @@ import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
+import com.android.internal.util.Preconditions;
import com.android.server.AnimationThread;
import com.android.server.DisplayThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.UiThread;
import com.android.server.wm.WindowManagerInternal;
+import com.android.server.wm.SurfaceAnimationThread;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -143,6 +149,7 @@ public final class DisplayManagerService extends SystemService {
private static final int MSG_REQUEST_TRAVERSAL = 4;
private static final int MSG_UPDATE_VIEWPORT = 5;
private static final int MSG_REGISTER_BRIGHTNESS_TRACKER = 6;
+ private static final int MSG_LOAD_BRIGHTNESS_CONFIGURATION = 7;
private final Context mContext;
private final DisplayManagerHandler mHandler;
@@ -217,6 +224,9 @@ public final class DisplayManagerService extends SystemService {
// The virtual display adapter, or null if not registered.
private VirtualDisplayAdapter mVirtualDisplayAdapter;
+ // The User ID of the current user
+ private @UserIdInt int mCurrentUserId;
+
// The stable device screen height and width. These are not tied to a specific display, even
// the default display, because they need to be stable over the course of the device's entire
// life, even if the default display changes (e.g. a new monitor is plugged into a PC-like
@@ -276,21 +286,24 @@ public final class DisplayManagerService extends SystemService {
mDisplayAdapterListener = new DisplayAdapterListener();
mSingleDisplayDemoMode = SystemProperties.getBoolean("persist.demo.singledisplay", false);
mDefaultDisplayDefaultColorMode = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_defaultDisplayDefaultColorMode);
+ com.android.internal.R.integer.config_defaultDisplayDefaultColorMode);
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mGlobalDisplayBrightness = pm.getDefaultScreenBrightnessSetting();
mBrightnessTracker = new BrightnessTracker(context, null);
+ mCurrentUserId = UserHandle.USER_SYSTEM;
}
public void setupSchedulerPolicies() {
// android.display and android.anim is critical to user experience and we should make sure
- // it is not in the default foregroup groups, add it to top-app to make sure it uses all the
- // cores and scheduling settings for top-app when it runs.
+ // it is not in the default foregroup groups, add it to top-app to make sure it uses all
+ // the cores and scheduling settings for top-app when it runs.
Process.setThreadGroupAndCpuset(DisplayThread.get().getThreadId(),
Process.THREAD_GROUP_TOP_APP);
Process.setThreadGroupAndCpuset(AnimationThread.get().getThreadId(),
Process.THREAD_GROUP_TOP_APP);
+ Process.setThreadGroupAndCpuset(SurfaceAnimationThread.get().getThreadId(),
+ Process.THREAD_GROUP_TOP_APP);
}
@Override
@@ -338,6 +351,19 @@ public final class DisplayManagerService extends SystemService {
}
}
+ @Override
+ public void onSwitchUser(@UserIdInt int newUserId) {
+ final int userSerial = getUserManager().getUserSerialNumber(newUserId);
+ synchronized (mSyncRoot) {
+ if (mCurrentUserId != newUserId) {
+ mCurrentUserId = newUserId;
+ BrightnessConfiguration config =
+ mPersistentDataStore.getBrightnessConfiguration(userSerial);
+ mDisplayPowerController.setBrightnessConfiguration(config);
+ }
+ }
+ }
+
// TODO: Use dependencies or a boot phase
public void windowManagerAndInputReady() {
synchronized (mSyncRoot) {
@@ -981,6 +1007,30 @@ public final class DisplayManagerService extends SystemService {
}
}
+ private void setBrightnessConfigurationForUserInternal(
+ @NonNull BrightnessConfiguration c, @UserIdInt int userId) {
+ final int userSerial = getUserManager().getUserSerialNumber(userId);
+ synchronized (mSyncRoot) {
+ try {
+ mPersistentDataStore.setBrightnessConfigurationForUser(c, userSerial);
+ } finally {
+ mPersistentDataStore.saveIfNeeded();
+ }
+ if (userId == mCurrentUserId) {
+ mDisplayPowerController.setBrightnessConfiguration(c);
+ }
+ }
+ }
+
+ private void loadBrightnessConfiguration() {
+ synchronized (mSyncRoot) {
+ final int userSerial = getUserManager().getUserSerialNumber(mCurrentUserId);
+ BrightnessConfiguration config =
+ mPersistentDataStore.getBrightnessConfiguration(userSerial);
+ mDisplayPowerController.setBrightnessConfiguration(config);
+ }
+ }
+
// Updates all existing logical displays given the current set of display devices.
// Removes invalid logical displays.
// Sends notifications if needed.
@@ -1225,6 +1275,10 @@ public final class DisplayManagerService extends SystemService {
return mProjectionService;
}
+ private UserManager getUserManager() {
+ return mContext.getSystemService(UserManager.class);
+ }
+
private void dumpInternal(PrintWriter pw) {
pw.println("DISPLAY MANAGER (dumpsys display)");
@@ -1367,6 +1421,10 @@ public final class DisplayManagerService extends SystemService {
case MSG_REGISTER_BRIGHTNESS_TRACKER:
mBrightnessTracker.start();
break;
+
+ case MSG_LOAD_BRIGHTNESS_CONFIGURATION:
+ loadBrightnessConfiguration();
+ break;
}
}
}
@@ -1752,14 +1810,30 @@ public final class DisplayManagerService extends SystemService {
}
@Override // Binder call
- public ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents() {
+ public ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents(String callingPackage) {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.BRIGHTNESS_SLIDER_USAGE,
"Permission to read brightness events.");
- int userId = UserHandle.getUserId(Binder.getCallingUid());
+
+ final int callingUid = Binder.getCallingUid();
+ AppOpsManager appOpsManager = mContext.getSystemService(AppOpsManager.class);
+ final int mode = appOpsManager.checkOp(AppOpsManager.OP_GET_USAGE_STATS,
+ callingUid, callingPackage);
+ final boolean hasUsageStats;
+ if (mode == AppOpsManager.MODE_DEFAULT) {
+ // The default behavior here is to check if PackageManager has given the app
+ // permission.
+ hasUsageStats = mContext.checkCallingPermission(
+ Manifest.permission.PACKAGE_USAGE_STATS)
+ == PackageManager.PERMISSION_GRANTED;
+ } else {
+ hasUsageStats = mode == AppOpsManager.MODE_ALLOWED;
+ }
+
+ final int userId = UserHandle.getUserId(callingUid);
final long token = Binder.clearCallingIdentity();
try {
- return mBrightnessTracker.getEvents(userId);
+ return mBrightnessTracker.getEvents(userId, hasUsageStats);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1780,6 +1854,27 @@ public final class DisplayManagerService extends SystemService {
}
}
+ @Override // Binder call
+ public void setBrightnessConfigurationForUser(
+ BrightnessConfiguration c, @UserIdInt int userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS,
+ "Permission required to change the display's brightness configuration");
+ if (userId != UserHandle.getCallingUserId()) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS,
+ "Permission required to change the display brightness"
+ + " configuration of another user");
+ }
+ Preconditions.checkNotNull(c);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ setBrightnessConfigurationForUserInternal(c, userId);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
private boolean validatePackageName(int uid, String packageName) {
if (packageName != null) {
String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
@@ -1851,18 +1946,24 @@ public final class DisplayManagerService extends SystemService {
mDisplayPowerController = new DisplayPowerController(
mContext, callbacks, handler, sensorManager, blanker);
}
+
+ mHandler.sendEmptyMessage(MSG_LOAD_BRIGHTNESS_CONFIGURATION);
}
@Override
public boolean requestPowerState(DisplayPowerRequest request,
boolean waitForNegativeProximity) {
- return mDisplayPowerController.requestPowerState(request,
- waitForNegativeProximity);
+ synchronized (mSyncRoot) {
+ return mDisplayPowerController.requestPowerState(request,
+ waitForNegativeProximity);
+ }
}
@Override
public boolean isProximitySensorAvailable() {
- return mDisplayPowerController.isProximitySensorAvailable();
+ synchronized (mSyncRoot) {
+ return mDisplayPowerController.isProximitySensorAvailable();
+ }
}
@Override
diff --git a/com/android/server/display/DisplayPowerController.java b/com/android/server/display/DisplayPowerController.java
index 29a007a3..a2d95482 100644
--- a/com/android/server/display/DisplayPowerController.java
+++ b/com/android/server/display/DisplayPowerController.java
@@ -26,10 +26,12 @@ import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.display.BrightnessConfiguration;
import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
import android.os.Handler;
@@ -93,6 +95,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
private static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 2;
private static final int MSG_SCREEN_ON_UNBLOCKED = 3;
private static final int MSG_SCREEN_OFF_UNBLOCKED = 4;
+ private static final int MSG_CONFIGURE_BRIGHTNESS = 5;
+ private static final int MSG_USER_SWITCH = 6;
private static final int PROXIMITY_UNKNOWN = -1;
private static final int PROXIMITY_NEGATIVE = 0;
@@ -285,6 +289,14 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
// The controller for the automatic brightness level.
private AutomaticBrightnessController mAutomaticBrightnessController;
+ // The default brightness configuration. Used for whenever we don't have a valid brightness
+ // configuration set. This is typically seen with users that don't have a brightness
+ // configuration that's different from the default.
+ private BrightnessConfiguration mDefaultBrightnessConfiguration;
+
+ // The current brightness configuration.
+ private BrightnessConfiguration mBrightnessConfiguration;
+
// Animators.
private ObjectAnimator mColorFadeOnAnimator;
private ObjectAnimator mColorFadeOffAnimator;
@@ -333,7 +345,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
screenBrightnessSettingMinimum, mScreenBrightnessDimConfig),
mScreenBrightnessDarkConfig);
- mScreenBrightnessRangeMaximum = PowerManager.BRIGHTNESS_ON;
+ mScreenBrightnessRangeMaximum = clampAbsoluteBrightness(resources.getInteger(
+ com.android.internal.R.integer.config_screenBrightnessSettingMaximum));
mUseSoftwareAutoBrightnessConfig = resources.getBoolean(
com.android.internal.R.bool.config_automatic_brightness_available);
@@ -348,60 +361,60 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
mSkipScreenOnBrightnessRamp = resources.getBoolean(
com.android.internal.R.bool.config_skipScreenOnBrightnessRamp);
- int lightSensorRate = resources.getInteger(
- com.android.internal.R.integer.config_autoBrightnessLightSensorRate);
- int initialLightSensorRate = resources.getInteger(
- com.android.internal.R.integer.config_autoBrightnessInitialLightSensorRate);
- if (initialLightSensorRate == -1) {
- initialLightSensorRate = lightSensorRate;
- } else if (initialLightSensorRate > lightSensorRate) {
- Slog.w(TAG, "Expected config_autoBrightnessInitialLightSensorRate ("
- + initialLightSensorRate + ") to be less than or equal to "
- + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ").");
- }
- long brighteningLightDebounce = resources.getInteger(
- com.android.internal.R.integer.config_autoBrightnessBrighteningLightDebounce);
- long darkeningLightDebounce = resources.getInteger(
- com.android.internal.R.integer.config_autoBrightnessDarkeningLightDebounce);
- boolean autoBrightnessResetAmbientLuxAfterWarmUp = resources.getBoolean(
- com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
- int ambientLightHorizon = resources.getInteger(
- com.android.internal.R.integer.config_autoBrightnessAmbientLightHorizon);
- float autoBrightnessAdjustmentMaxGamma = resources.getFraction(
- com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
- 1, 1);
-
- int[] brightLevels = resources.getIntArray(
- com.android.internal.R.array.config_dynamicHysteresisBrightLevels);
- int[] darkLevels = resources.getIntArray(
- com.android.internal.R.array.config_dynamicHysteresisDarkLevels);
- int[] luxLevels = resources.getIntArray(
- com.android.internal.R.array.config_dynamicHysteresisLuxLevels);
- HysteresisLevels dynamicHysteresis = new HysteresisLevels(
- brightLevels, darkLevels, luxLevels);
-
if (mUseSoftwareAutoBrightnessConfig) {
- int[] lux = resources.getIntArray(
- com.android.internal.R.array.config_autoBrightnessLevels);
- int[] screenBrightness = resources.getIntArray(
+ float[] luxLevels = getLuxLevels(resources.getIntArray(
+ com.android.internal.R.array.config_autoBrightnessLevels));
+ int[] backlightValues = resources.getIntArray(
com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
- int lightSensorWarmUpTimeConfig = resources.getInteger(
- com.android.internal.R.integer.config_lightSensorWarmupTime);
+ float[] brightnessValuesNits = getFloatArray(resources.obtainTypedArray(
+ com.android.internal.R.array.config_autoBrightnessDisplayValuesNits));
+
+ final float screenMinimumNits = resources.getFloat(
+ com.android.internal.R.dimen.config_screenBrightnessMinimumNits);
+ final float screenMaximumNits = resources.getFloat(
+ com.android.internal.R.dimen.config_screenBrightnessMaximumNits);
+
final float dozeScaleFactor = resources.getFraction(
com.android.internal.R.fraction.config_screenAutoBrightnessDozeScaleFactor,
1, 1);
- Spline screenAutoBrightnessSpline = createAutoBrightnessSpline(lux, screenBrightness);
- if (screenAutoBrightnessSpline == null) {
- Slog.e(TAG, "Error in config.xml. config_autoBrightnessLcdBacklightValues "
- + "(size " + screenBrightness.length + ") "
- + "must be monotic and have exactly one more entry than "
- + "config_autoBrightnessLevels (size " + lux.length + ") "
- + "which must be strictly increasing. "
- + "Auto-brightness will be disabled.");
- mUseSoftwareAutoBrightnessConfig = false;
- } else {
- int bottom = clampAbsoluteBrightness(screenBrightness[0]);
+ int[] brightLevels = resources.getIntArray(
+ com.android.internal.R.array.config_dynamicHysteresisBrightLevels);
+ int[] darkLevels = resources.getIntArray(
+ com.android.internal.R.array.config_dynamicHysteresisDarkLevels);
+ int[] luxHysteresisLevels = resources.getIntArray(
+ com.android.internal.R.array.config_dynamicHysteresisLuxLevels);
+ HysteresisLevels dynamicHysteresis = new HysteresisLevels(
+ brightLevels, darkLevels, luxHysteresisLevels);
+
+ long brighteningLightDebounce = resources.getInteger(
+ com.android.internal.R.integer.config_autoBrightnessBrighteningLightDebounce);
+ long darkeningLightDebounce = resources.getInteger(
+ com.android.internal.R.integer.config_autoBrightnessDarkeningLightDebounce);
+ boolean autoBrightnessResetAmbientLuxAfterWarmUp = resources.getBoolean(
+ com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
+ int ambientLightHorizon = resources.getInteger(
+ com.android.internal.R.integer.config_autoBrightnessAmbientLightHorizon);
+ float autoBrightnessAdjustmentMaxGamma = resources.getFraction(
+ com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
+ 1, 1);
+
+ int lightSensorWarmUpTimeConfig = resources.getInteger(
+ com.android.internal.R.integer.config_lightSensorWarmupTime);
+ int lightSensorRate = resources.getInteger(
+ com.android.internal.R.integer.config_autoBrightnessLightSensorRate);
+ int initialLightSensorRate = resources.getInteger(
+ com.android.internal.R.integer.config_autoBrightnessInitialLightSensorRate);
+ if (initialLightSensorRate == -1) {
+ initialLightSensorRate = lightSensorRate;
+ } else if (initialLightSensorRate > lightSensorRate) {
+ Slog.w(TAG, "Expected config_autoBrightnessInitialLightSensorRate ("
+ + initialLightSensorRate + ") to be less than or equal to "
+ + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ").");
+ }
+
+ if (backlightValues != null && backlightValues.length > 0) {
+ final int bottom = backlightValues[0];
if (mScreenBrightnessDarkConfig > bottom) {
Slog.w(TAG, "config_screenBrightnessDark (" + mScreenBrightnessDarkConfig
+ ") should be less than or equal to the first value of "
@@ -411,13 +424,24 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
if (bottom < screenBrightnessRangeMinimum) {
screenBrightnessRangeMinimum = bottom;
}
+ }
+
+ float[] nitsRange = { screenMinimumNits, screenMaximumNits };
+ int[] backlightRange = { screenBrightnessRangeMinimum, mScreenBrightnessRangeMaximum };
+
+ BrightnessMappingStrategy mapper = BrightnessMappingStrategy.create(
+ luxLevels, backlightValues, brightnessValuesNits,
+ nitsRange, backlightRange);
+ if (mapper != null) {
mAutomaticBrightnessController = new AutomaticBrightnessController(this,
- handler.getLooper(), sensorManager, screenAutoBrightnessSpline,
- lightSensorWarmUpTimeConfig, screenBrightnessRangeMinimum,
- mScreenBrightnessRangeMaximum, dozeScaleFactor, lightSensorRate,
- initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
+ handler.getLooper(), sensorManager, mapper, lightSensorWarmUpTimeConfig,
+ screenBrightnessRangeMinimum, mScreenBrightnessRangeMaximum,
+ dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+ brighteningLightDebounce, darkeningLightDebounce,
autoBrightnessResetAmbientLuxAfterWarmUp, ambientLightHorizon,
autoBrightnessAdjustmentMaxGamma, dynamicHysteresis);
+ } else {
+ mUseSoftwareAutoBrightnessConfig = false;
}
}
@@ -444,6 +468,25 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
}
+ private static float[] getLuxLevels(int[] lux) {
+ // The first control point is implicit and always at 0 lux.
+ float[] levels = new float[lux.length + 1];
+ for (int i = 0; i < lux.length; i++) {
+ levels[i + 1] = (float) lux[i];
+ }
+ return levels;
+ }
+
+ private static float[] getFloatArray(TypedArray array) {
+ final int N = array.length();
+ float[] vals = new float[N];
+ for (int i = 0; i < N; i++) {
+ vals[i] = array.getFloat(i, -1.0f);
+ }
+ array.recycle();
+ return vals;
+ }
+
/**
* Returns true if the proximity sensor screen-off function is available.
*/
@@ -512,7 +555,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
if (!mPendingUpdatePowerStateLocked) {
mPendingUpdatePowerStateLocked = true;
Message msg = mHandler.obtainMessage(MSG_UPDATE_POWER_STATE);
- msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
}
@@ -691,8 +733,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
final boolean userInitiatedChange = autoBrightnessAdjustmentChanged
&& mPowerRequest.brightnessSetByUser;
mAutomaticBrightnessController.configure(autoBrightnessEnabled,
- mPowerRequest.screenAutoBrightnessAdjustment, state != Display.STATE_ON,
- userInitiatedChange);
+ mBrightnessConfiguration, mPowerRequest.screenAutoBrightnessAdjustment,
+ state != Display.STATE_ON, userInitiatedChange);
}
// Apply brightness boost.
@@ -874,6 +916,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
sendUpdatePowerState();
}
+ public void setBrightnessConfiguration(BrightnessConfiguration c) {
+ Message msg = mHandler.obtainMessage(MSG_CONFIGURE_BRIGHTNESS, c);
+ msg.sendToTarget();
+ }
+
private void blockScreenOn() {
if (mPendingScreenOnUnblocker == null) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_TRACE_NAME, 0);
@@ -1241,7 +1288,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
// Need to wait a little longer.
// Debounce again later. We continue holding a wake lock while waiting.
Message msg = mHandler.obtainMessage(MSG_PROXIMITY_SENSOR_DEBOUNCED);
- msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, mPendingProximityDebounceTime);
}
}
@@ -1402,39 +1448,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
}
}
- private static Spline createAutoBrightnessSpline(int[] lux, int[] brightness) {
- if (lux == null || lux.length == 0 || brightness == null || brightness.length == 0) {
- Slog.e(TAG, "Could not create auto-brightness spline.");
- return null;
- }
- try {
- final int n = brightness.length;
- float[] x = new float[n];
- float[] y = new float[n];
- y[0] = normalizeAbsoluteBrightness(brightness[0]);
- for (int i = 1; i < n; i++) {
- x[i] = lux[i - 1];
- y[i] = normalizeAbsoluteBrightness(brightness[i]);
- }
-
- Spline spline = Spline.createSpline(x, y);
- if (DEBUG) {
- Slog.d(TAG, "Auto-brightness spline: " + spline);
- for (float v = 1f; v < lux[lux.length - 1] * 1.25f; v *= 1.25f) {
- Slog.d(TAG, String.format(" %7.1f: %7.1f", v, spline.interpolate(v)));
- }
- }
- return spline;
- } catch (IllegalArgumentException ex) {
- Slog.e(TAG, "Could not create auto-brightness spline.", ex);
- return null;
- }
- }
-
- private static float normalizeAbsoluteBrightness(int value) {
- return (float)clampAbsoluteBrightness(value) / PowerManager.BRIGHTNESS_ON;
- }
-
private static int clampAbsoluteBrightness(int value) {
return MathUtils.constrain(value, PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON);
}
@@ -1467,6 +1480,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
updatePowerState();
}
break;
+ case MSG_CONFIGURE_BRIGHTNESS:
+ BrightnessConfiguration c = (BrightnessConfiguration) msg.obj;
+ mBrightnessConfiguration = c != null ? c : mDefaultBrightnessConfiguration;
+ updatePowerState();
+ break;
}
}
}
@@ -1492,17 +1510,14 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
@Override
public void onScreenOn() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
- msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
}
private final class ScreenOffUnblocker implements WindowManagerPolicy.ScreenOffListener {
-
@Override
public void onScreenOff() {
Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
- msg.setAsynchronous(true);
mHandler.sendMessage(msg);
}
}
diff --git a/com/android/server/display/DisplayTransformManager.java b/com/android/server/display/DisplayTransformManager.java
index 338e3311..000fcf3e 100644
--- a/com/android/server/display/DisplayTransformManager.java
+++ b/com/android/server/display/DisplayTransformManager.java
@@ -222,7 +222,7 @@ public class DisplayTransformManager {
return SystemProperties.getBoolean(PERSISTENT_PROPERTY_NATIVE_MODE, false);
}
- public boolean setColorMode(int colorMode) {
+ public boolean setColorMode(int colorMode, float[] nightDisplayMatrix) {
if (colorMode == ColorDisplayController.COLOR_MODE_NATURAL) {
applySaturation(COLOR_SATURATION_NATURAL);
setNativeMode(false);
@@ -233,6 +233,7 @@ public class DisplayTransformManager {
applySaturation(COLOR_SATURATION_NATURAL);
setNativeMode(true);
}
+ setColorMatrix(LEVEL_COLOR_MATRIX_NIGHT_DISPLAY, nightDisplayMatrix);
updateConfiguration();
diff --git a/com/android/server/display/PersistentDataStore.java b/com/android/server/display/PersistentDataStore.java
index 34c8e22a..49b4465e 100644
--- a/com/android/server/display/PersistentDataStore.java
+++ b/com/android/server/display/PersistentDataStore.java
@@ -24,12 +24,17 @@ import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import android.graphics.Point;
+import android.hardware.display.BrightnessConfiguration;
import android.hardware.display.WifiDisplay;
import android.util.AtomicFile;
import android.util.Slog;
+import android.util.SparseArray;
+import android.util.Pair;
import android.util.Xml;
import android.view.Display;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
@@ -37,10 +42,12 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import libcore.io.IoUtils;
@@ -57,14 +64,22 @@ import libcore.util.Objects;
* &lt;wifi-display deviceAddress="00:00:00:00:00:00" deviceName="XXXX" deviceAlias="YYYY" />
* &lt;remembered-wifi-displays>
* &lt;display-states>
- * &lt;display>
+ * &lt;display unique-id="XXXXXXX">
* &lt;color-mode>0&lt;/color-mode>
* &lt;/display>
* &lt;/display-states>
* &lt;stable-device-values>
- * &lt;stable-display-height>1920&lt;stable-display-height>
- * &lt;stable-display-width>1080&lt;stable-display-width>
+ * &lt;stable-display-height>1920&lt;/stable-display-height>
+ * &lt;stable-display-width>1080&lt;/stable-display-width>
* &lt;/stable-device-values>
+ * &lt;brightness-configurations>
+ * &lt;brightness-configuration user-id="0">
+ * &lt;brightness-curve>
+ * &lt;brightness-point lux="0" nits="13.25"/>
+ * &lt;brightness-point lux="20" nits="35.94"/>
+ * &lt;/brightness-curve>
+ * &lt;/brightness-configuration>
+ * &lt;/brightness-configurations>
* &lt;/display-manager-state>
* </code>
*
@@ -73,6 +88,31 @@ import libcore.util.Objects;
final class PersistentDataStore {
static final String TAG = "DisplayManager";
+ private static final String TAG_DISPLAY_MANAGER_STATE = "display-manager-state";
+
+ private static final String TAG_REMEMBERED_WIFI_DISPLAYS = "remembered-wifi-displays";
+ private static final String TAG_WIFI_DISPLAY = "wifi-display";
+ private static final String ATTR_DEVICE_ADDRESS = "deviceAddress";
+ private static final String ATTR_DEVICE_NAME = "deviceName";
+ private static final String ATTR_DEVICE_ALIAS = "deviceAlias";
+
+ private static final String TAG_DISPLAY_STATES = "display-states";
+ private static final String TAG_DISPLAY = "display";
+ private static final String TAG_COLOR_MODE = "color-mode";
+ private static final String ATTR_UNIQUE_ID = "unique-id";
+
+ private static final String TAG_STABLE_DEVICE_VALUES = "stable-device-values";
+ private static final String TAG_STABLE_DISPLAY_HEIGHT = "stable-display-height";
+ private static final String TAG_STABLE_DISPLAY_WIDTH = "stable-display-width";
+
+ private static final String TAG_BRIGHTNESS_CONFIGURATIONS = "brightness-configurations";
+ private static final String TAG_BRIGHTNESS_CONFIGURATION = "brightness-configuration";
+ private static final String TAG_BRIGHTNESS_CURVE = "brightness-curve";
+ private static final String TAG_BRIGHTNESS_POINT = "brightness-point";
+ private static final String ATTR_USER_SERIAL = "user-serial";
+ private static final String ATTR_LUX = "lux";
+ private static final String ATTR_NITS = "nits";
+
// Remembered Wifi display devices.
private ArrayList<WifiDisplay> mRememberedWifiDisplays = new ArrayList<WifiDisplay>();
@@ -83,8 +123,8 @@ final class PersistentDataStore {
// Display values which should be stable across the device's lifetime.
private final StableDeviceValues mStableDeviceValues = new StableDeviceValues();
- // The atomic file used to safely read or write the file.
- private final AtomicFile mAtomicFile;
+ // Brightness configuration by user
+ private BrightnessConfigurations mBrightnessConfigurations = new BrightnessConfigurations();
// True if the data has been loaded.
private boolean mLoaded;
@@ -92,8 +132,16 @@ final class PersistentDataStore {
// True if there are changes to be saved.
private boolean mDirty;
+ // The interface for methods which should be replaced by the test harness.
+ private Injector mInjector;
+
public PersistentDataStore() {
- mAtomicFile = new AtomicFile(new File("/data/system/display-manager-state.xml"));
+ this(new Injector());
+ }
+
+ @VisibleForTesting
+ PersistentDataStore(Injector injector) {
+ mInjector = injector;
}
public void saveIfNeeded() {
@@ -225,6 +273,18 @@ final class PersistentDataStore {
}
}
+ public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userSerial) {
+ loadIfNeeded();
+ if (mBrightnessConfigurations.setBrightnessConfigurationForUser(c, userSerial)) {
+ setDirty();
+ }
+ }
+
+ public BrightnessConfiguration getBrightnessConfiguration(int userSerial) {
+ loadIfNeeded();
+ return mBrightnessConfigurations.getBrightnessConfiguration(userSerial);
+ }
+
private DisplayState getDisplayState(String uniqueId, boolean createIfAbsent) {
loadIfNeeded();
DisplayState state = mDisplayStates.get(uniqueId);
@@ -256,7 +316,7 @@ final class PersistentDataStore {
final InputStream is;
try {
- is = mAtomicFile.openRead();
+ is = mInjector.openRead();
} catch (FileNotFoundException ex) {
return;
}
@@ -278,9 +338,9 @@ final class PersistentDataStore {
}
private void save() {
- final FileOutputStream os;
+ final OutputStream os;
try {
- os = mAtomicFile.startWrite();
+ os = mInjector.startWrite();
boolean success = false;
try {
XmlSerializer serializer = new FastXmlSerializer();
@@ -289,11 +349,7 @@ final class PersistentDataStore {
serializer.flush();
success = true;
} finally {
- if (success) {
- mAtomicFile.finishWrite(os);
- } else {
- mAtomicFile.failWrite(os);
- }
+ mInjector.finishWrite(os, success);
}
} catch (IOException ex) {
Slog.w(TAG, "Failed to save display manager persistent store data.", ex);
@@ -302,18 +358,21 @@ final class PersistentDataStore {
private void loadFromXml(XmlPullParser parser)
throws IOException, XmlPullParserException {
- XmlUtils.beginDocument(parser, "display-manager-state");
+ XmlUtils.beginDocument(parser, TAG_DISPLAY_MANAGER_STATE);
final int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- if (parser.getName().equals("remembered-wifi-displays")) {
+ if (parser.getName().equals(TAG_REMEMBERED_WIFI_DISPLAYS)) {
loadRememberedWifiDisplaysFromXml(parser);
}
- if (parser.getName().equals("display-states")) {
+ if (parser.getName().equals(TAG_DISPLAY_STATES)) {
loadDisplaysFromXml(parser);
}
- if (parser.getName().equals("stable-device-values")) {
+ if (parser.getName().equals(TAG_STABLE_DEVICE_VALUES)) {
mStableDeviceValues.loadFromXml(parser);
}
+ if (parser.getName().equals(TAG_BRIGHTNESS_CONFIGURATIONS)) {
+ mBrightnessConfigurations.loadFromXml(parser);
+ }
}
}
@@ -321,10 +380,10 @@ final class PersistentDataStore {
throws IOException, XmlPullParserException {
final int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- if (parser.getName().equals("wifi-display")) {
- String deviceAddress = parser.getAttributeValue(null, "deviceAddress");
- String deviceName = parser.getAttributeValue(null, "deviceName");
- String deviceAlias = parser.getAttributeValue(null, "deviceAlias");
+ if (parser.getName().equals(TAG_WIFI_DISPLAY)) {
+ String deviceAddress = parser.getAttributeValue(null, ATTR_DEVICE_ADDRESS);
+ String deviceName = parser.getAttributeValue(null, ATTR_DEVICE_NAME);
+ String deviceAlias = parser.getAttributeValue(null, ATTR_DEVICE_ALIAS);
if (deviceAddress == null || deviceName == null) {
throw new XmlPullParserException(
"Missing deviceAddress or deviceName attribute on wifi-display.");
@@ -345,8 +404,8 @@ final class PersistentDataStore {
throws IOException, XmlPullParserException {
final int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- if (parser.getName().equals("display")) {
- String uniqueId = parser.getAttributeValue(null, "unique-id");
+ if (parser.getName().equals(TAG_DISPLAY)) {
+ String uniqueId = parser.getAttributeValue(null, ATTR_UNIQUE_ID);
if (uniqueId == null) {
throw new XmlPullParserException(
"Missing unique-id attribute on display.");
@@ -365,32 +424,35 @@ final class PersistentDataStore {
private void saveToXml(XmlSerializer serializer) throws IOException {
serializer.startDocument(null, true);
serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
- serializer.startTag(null, "display-manager-state");
- serializer.startTag(null, "remembered-wifi-displays");
+ serializer.startTag(null, TAG_DISPLAY_MANAGER_STATE);
+ serializer.startTag(null, TAG_REMEMBERED_WIFI_DISPLAYS);
for (WifiDisplay display : mRememberedWifiDisplays) {
- serializer.startTag(null, "wifi-display");
- serializer.attribute(null, "deviceAddress", display.getDeviceAddress());
- serializer.attribute(null, "deviceName", display.getDeviceName());
+ serializer.startTag(null, TAG_WIFI_DISPLAY);
+ serializer.attribute(null, ATTR_DEVICE_ADDRESS, display.getDeviceAddress());
+ serializer.attribute(null, ATTR_DEVICE_NAME, display.getDeviceName());
if (display.getDeviceAlias() != null) {
- serializer.attribute(null, "deviceAlias", display.getDeviceAlias());
+ serializer.attribute(null, ATTR_DEVICE_ALIAS, display.getDeviceAlias());
}
- serializer.endTag(null, "wifi-display");
+ serializer.endTag(null, TAG_WIFI_DISPLAY);
}
- serializer.endTag(null, "remembered-wifi-displays");
- serializer.startTag(null, "display-states");
+ serializer.endTag(null, TAG_REMEMBERED_WIFI_DISPLAYS);
+ serializer.startTag(null, TAG_DISPLAY_STATES);
for (Map.Entry<String, DisplayState> entry : mDisplayStates.entrySet()) {
final String uniqueId = entry.getKey();
final DisplayState state = entry.getValue();
- serializer.startTag(null, "display");
- serializer.attribute(null, "unique-id", uniqueId);
+ serializer.startTag(null, TAG_DISPLAY);
+ serializer.attribute(null, ATTR_UNIQUE_ID, uniqueId);
state.saveToXml(serializer);
- serializer.endTag(null, "display");
+ serializer.endTag(null, TAG_DISPLAY);
}
- serializer.endTag(null, "display-states");
- serializer.startTag(null, "stable-device-values");
+ serializer.endTag(null, TAG_DISPLAY_STATES);
+ serializer.startTag(null, TAG_STABLE_DEVICE_VALUES);
mStableDeviceValues.saveToXml(serializer);
- serializer.endTag(null, "stable-device-values");
- serializer.endTag(null, "display-manager-state");
+ serializer.endTag(null, TAG_STABLE_DEVICE_VALUES);
+ serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
+ mBrightnessConfigurations.saveToXml(serializer);
+ serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
+ serializer.endTag(null, TAG_DISPLAY_MANAGER_STATE);
serializer.endDocument();
}
@@ -411,6 +473,8 @@ final class PersistentDataStore {
}
pw.println(" StableDeviceValues:");
mStableDeviceValues.dump(pw, " ");
+ pw.println(" BrightnessConfigurations:");
+ mBrightnessConfigurations.dump(pw, " ");
}
private static final class DisplayState {
@@ -433,7 +497,7 @@ final class PersistentDataStore {
final int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- if (parser.getName().equals("color-mode")) {
+ if (parser.getName().equals(TAG_COLOR_MODE)) {
String value = parser.nextText();
mColorMode = Integer.parseInt(value);
}
@@ -441,9 +505,9 @@ final class PersistentDataStore {
}
public void saveToXml(XmlSerializer serializer) throws IOException {
- serializer.startTag(null, "color-mode");
+ serializer.startTag(null, TAG_COLOR_MODE);
serializer.text(Integer.toString(mColorMode));
- serializer.endTag(null, "color-mode");
+ serializer.endTag(null, TAG_COLOR_MODE);
}
public void dump(final PrintWriter pw, final String prefix) {
@@ -472,10 +536,10 @@ final class PersistentDataStore {
final int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
switch (parser.getName()) {
- case "stable-display-width":
+ case TAG_STABLE_DISPLAY_WIDTH:
mWidth = loadIntValue(parser);
break;
- case "stable-display-height":
+ case TAG_STABLE_DISPLAY_HEIGHT:
mHeight = loadIntValue(parser);
break;
}
@@ -494,12 +558,12 @@ final class PersistentDataStore {
public void saveToXml(XmlSerializer serializer) throws IOException {
if (mWidth > 0 && mHeight > 0) {
- serializer.startTag(null, "stable-display-width");
+ serializer.startTag(null, TAG_STABLE_DISPLAY_WIDTH);
serializer.text(Integer.toString(mWidth));
- serializer.endTag(null, "stable-display-width");
- serializer.startTag(null, "stable-display-height");
+ serializer.endTag(null, TAG_STABLE_DISPLAY_WIDTH);
+ serializer.startTag(null, TAG_STABLE_DISPLAY_HEIGHT);
serializer.text(Integer.toString(mHeight));
- serializer.endTag(null, "stable-display-height");
+ serializer.endTag(null, TAG_STABLE_DISPLAY_HEIGHT);
}
}
@@ -508,4 +572,158 @@ final class PersistentDataStore {
pw.println(prefix + "StableDisplayHeight=" + mHeight);
}
}
+
+ private static final class BrightnessConfigurations {
+ // Maps from a user ID to the users' given brightness configuration
+ private SparseArray<BrightnessConfiguration> mConfigurations;
+
+ public BrightnessConfigurations() {
+ mConfigurations = new SparseArray<>();
+ }
+
+ private boolean setBrightnessConfigurationForUser(BrightnessConfiguration c,
+ int userSerial) {
+ BrightnessConfiguration currentConfig = mConfigurations.get(userSerial);
+ if (currentConfig == null || !currentConfig.equals(c)) {
+ mConfigurations.put(userSerial, c);
+ return true;
+ }
+ return false;
+ }
+
+ public BrightnessConfiguration getBrightnessConfiguration(int userSerial) {
+ return mConfigurations.get(userSerial);
+ }
+
+ public void loadFromXml(XmlPullParser parser) throws IOException, XmlPullParserException {
+ final int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (TAG_BRIGHTNESS_CONFIGURATION.equals(parser.getName())) {
+ int userSerial;
+ try {
+ userSerial = Integer.parseInt(
+ parser.getAttributeValue(null, ATTR_USER_SERIAL));
+ } catch (NumberFormatException nfe) {
+ userSerial= -1;
+ Slog.e(TAG, "Failed to read in brightness configuration", nfe);
+ }
+
+ try {
+ BrightnessConfiguration config = loadConfigurationFromXml(parser);
+ if (userSerial>= 0 && config != null) {
+ mConfigurations.put(userSerial, config);
+ }
+ } catch (IllegalArgumentException iae) {
+ Slog.e(TAG, "Failed to load brightness configuration!", iae);
+ }
+ }
+ }
+ }
+
+ private static BrightnessConfiguration loadConfigurationFromXml(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ final int outerDepth = parser.getDepth();
+ final BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (TAG_BRIGHTNESS_CURVE.equals(parser.getName())) {
+ Pair<float[], float[]> curve = loadCurveFromXml(parser, builder);
+ builder.setCurve(curve.first /*lux*/, curve.second /*nits*/);
+ }
+ }
+ return builder.build();
+ }
+
+ private static Pair<float[], float[]> loadCurveFromXml(XmlPullParser parser,
+ BrightnessConfiguration.Builder builder)
+ throws IOException, XmlPullParserException {
+ final int outerDepth = parser.getDepth();
+ List<Float> luxLevels = new ArrayList<>();
+ List<Float> nitLevels = new ArrayList<>();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (TAG_BRIGHTNESS_POINT.equals(parser.getName())) {
+ luxLevels.add(loadFloat(parser.getAttributeValue(null, ATTR_LUX)));
+ nitLevels.add(loadFloat(parser.getAttributeValue(null, ATTR_NITS)));
+ }
+ }
+ final int N = luxLevels.size();
+ float[] lux = new float[N];
+ float[] nits = new float[N];
+ for (int i = 0; i < N; i++) {
+ lux[i] = luxLevels.get(i);
+ nits[i] = nitLevels.get(i);
+ }
+ return Pair.create(lux, nits);
+ }
+
+ private static float loadFloat(String val) {
+ try {
+ return Float.parseFloat(val);
+ } catch (NullPointerException | NumberFormatException e) {
+ Slog.e(TAG, "Failed to parse float loading brightness config", e);
+ return Float.NEGATIVE_INFINITY;
+ }
+ }
+
+ public void saveToXml(XmlSerializer serializer) throws IOException {
+ for (int i = 0; i < mConfigurations.size(); i++) {
+ final int userSerial= mConfigurations.keyAt(i);
+ final BrightnessConfiguration config = mConfigurations.valueAt(i);
+
+ serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATION);
+ serializer.attribute(null, ATTR_USER_SERIAL, Integer.toString(userSerial));
+ saveConfigurationToXml(serializer, config);
+ serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATION);
+ }
+ }
+
+ private static void saveConfigurationToXml(XmlSerializer serializer,
+ BrightnessConfiguration config) throws IOException {
+ serializer.startTag(null, TAG_BRIGHTNESS_CURVE);
+ final Pair<float[], float[]> curve = config.getCurve();
+ for (int i = 0; i < curve.first.length; i++) {
+ serializer.startTag(null, TAG_BRIGHTNESS_POINT);
+ serializer.attribute(null, ATTR_LUX, Float.toString(curve.first[i]));
+ serializer.attribute(null, ATTR_NITS, Float.toString(curve.second[i]));
+ serializer.endTag(null, TAG_BRIGHTNESS_POINT);
+ }
+ serializer.endTag(null, TAG_BRIGHTNESS_CURVE);
+ }
+
+ public void dump(final PrintWriter pw, final String prefix) {
+ for (int i = 0; i < mConfigurations.size(); i++) {
+ final int userSerial= mConfigurations.keyAt(i);
+ pw.println(prefix + "User " + userSerial + ":");
+ pw.println(prefix + " " + mConfigurations.valueAt(i));
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static class Injector {
+ private final AtomicFile mAtomicFile;
+
+ public Injector() {
+ mAtomicFile = new AtomicFile(new File("/data/system/display-manager-state.xml"));
+ }
+
+ public InputStream openRead() throws FileNotFoundException {
+ return mAtomicFile.openRead();
+ }
+
+ public OutputStream startWrite() throws IOException {
+ return mAtomicFile.startWrite();
+ }
+
+ public void finishWrite(OutputStream os, boolean success) {
+ if (!(os instanceof FileOutputStream)) {
+ throw new IllegalArgumentException("Unexpected OutputStream as argument: " + os);
+ }
+ FileOutputStream fos = (FileOutputStream) os;
+ if (success) {
+ mAtomicFile.finishWrite(fos);
+ } else {
+ mAtomicFile.failWrite(fos);
+ }
+ }
+ }
}
diff --git a/com/android/server/job/JobSchedulerInternal.java b/com/android/server/job/JobSchedulerInternal.java
index 9bcf208a..c97eeaf3 100644
--- a/com/android/server/job/JobSchedulerInternal.java
+++ b/com/android/server/job/JobSchedulerInternal.java
@@ -44,6 +44,11 @@ public interface JobSchedulerInternal {
List<JobInfo> getSystemScheduledPendingJobs();
/**
+ * Cancel the jobs for a given uid (e.g. when app data is cleared)
+ */
+ void cancelJobsForUid(int uid, String reason);
+
+ /**
* These are for activity manager to communicate to use what is currently performing backups.
*/
void addBackingUpUid(int uid);
diff --git a/com/android/server/job/JobSchedulerService.java b/com/android/server/job/JobSchedulerService.java
index 4af86a05..bcb57eff 100644
--- a/com/android/server/job/JobSchedulerService.java
+++ b/com/android/server/job/JobSchedulerService.java
@@ -29,7 +29,7 @@ import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.app.job.JobWorkItem;
-import android.app.usage.AppStandby;
+import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal;
import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
import android.content.BroadcastReceiver;
@@ -279,8 +279,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS;
private static final long DEFAULT_MIN_EXP_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS;
private static final long DEFAULT_STANDBY_HEARTBEAT_TIME = 11 * 60 * 1000L;
- private static final int DEFAULT_STANDBY_WORKING_BEATS = 5; // ~ 1 hour, with 11-min beats
- private static final int DEFAULT_STANDBY_FREQUENT_BEATS = 31; // ~ 6 hours
+ private static final int DEFAULT_STANDBY_WORKING_BEATS = 11; // ~ 2 hours, with 11min beats
+ private static final int DEFAULT_STANDBY_FREQUENT_BEATS = 43; // ~ 8 hours
private static final int DEFAULT_STANDBY_RARE_BEATS = 130; // ~ 24 hours
/**
@@ -1721,16 +1721,29 @@ public final class JobSchedulerService extends com.android.server.SystemService
// If the app is in a non-active standby bucket, make sure we've waited
// an appropriate amount of time since the last invocation
- if (mHeartbeat < mNextBucketHeartbeat[job.getStandbyBucket()]) {
- // TODO: log/trace that we're deferring the job due to bucketing if we hit this
- if (job.getWhenStandbyDeferred() == 0) {
+ final int bucket = job.getStandbyBucket();
+ if (mHeartbeat < mNextBucketHeartbeat[bucket]) {
+ // Only skip this job if it's still waiting for the end of its (initial) nominal
+ // bucket interval. Once it's waited that long, we let it go ahead and clear.
+ // The final (NEVER) bucket is special; we never age those apps' jobs into
+ // runnability.
+ if (bucket >= mConstants.STANDBY_BEATS.length
+ || (mHeartbeat < job.getBaseHeartbeat() + mConstants.STANDBY_BEATS[bucket])) {
+ // TODO: log/trace that we're deferring the job due to bucketing if we hit this
+ if (job.getWhenStandbyDeferred() == 0) {
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, "Bucket deferral: " + mHeartbeat + " < "
+ + mNextBucketHeartbeat[job.getStandbyBucket()] + " for " + job);
+ }
+ job.setWhenStandbyDeferred(sElapsedRealtimeClock.millis());
+ }
+ return false;
+ } else {
if (DEBUG_STANDBY) {
- Slog.v(TAG, "Bucket deferral: " + mHeartbeat + " < "
- + mNextBucketHeartbeat[job.getStandbyBucket()] + " for " + job);
+ Slog.v(TAG, "Bucket deferred job aged into runnability at "
+ + mHeartbeat + " : " + job);
}
- job.setWhenStandbyDeferred(sElapsedRealtimeClock.millis());
}
- return false;
}
// The expensive check last: validate that the defined package+service is
@@ -1984,6 +1997,11 @@ public final class JobSchedulerService extends com.android.server.SystemService
}
@Override
+ public void cancelJobsForUid(int uid, String reason) {
+ JobSchedulerService.this.cancelJobsForUid(uid, reason);
+ }
+
+ @Override
public void addBackingUpUid(int uid) {
synchronized (mLock) {
// No need to actually do anything here, since for a full backup the
@@ -2074,10 +2092,10 @@ public final class JobSchedulerService extends com.android.server.SystemService
public static int standbyBucketToBucketIndex(int bucket) {
// Normalize AppStandby constants to indices into our bookkeeping
- if (bucket == AppStandby.STANDBY_BUCKET_NEVER) return 4;
- else if (bucket >= AppStandby.STANDBY_BUCKET_RARE) return 3;
- else if (bucket >= AppStandby.STANDBY_BUCKET_FREQUENT) return 2;
- else if (bucket >= AppStandby.STANDBY_BUCKET_WORKING_SET) return 1;
+ if (bucket == UsageStatsManager.STANDBY_BUCKET_NEVER) return 4;
+ else if (bucket >= UsageStatsManager.STANDBY_BUCKET_RARE) return 3;
+ else if (bucket >= UsageStatsManager.STANDBY_BUCKET_FREQUENT) return 2;
+ else if (bucket >= UsageStatsManager.STANDBY_BUCKET_WORKING_SET) return 1;
else return 0;
}
diff --git a/com/android/server/job/JobStore.java b/com/android/server/job/JobStore.java
index 219bc611..36cacd7a 100644
--- a/com/android/server/job/JobStore.java
+++ b/com/android/server/job/JobStore.java
@@ -1149,8 +1149,10 @@ public final class JobStore {
public void forEachJob(JobStatusFunctor functor) {
for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
- for (int i = jobs.size() - 1; i >= 0; i--) {
- functor.process(jobs.valueAt(i));
+ if (jobs != null) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ functor.process(jobs.valueAt(i));
+ }
}
}
}
diff --git a/com/android/server/location/ContextHubClientBroker.java b/com/android/server/location/ContextHubClientBroker.java
index 41d9feb9..9640e042 100644
--- a/com/android/server/location/ContextHubClientBroker.java
+++ b/com/android/server/location/ContextHubClientBroker.java
@@ -179,7 +179,7 @@ public class ContextHubClientBroker extends IContextHubClient.Stub
}
/**
- * Handles a nanoapp load event.
+ * Notifies the client of a nanoapp load event if the connection is open.
*
* @param nanoAppId the ID of the nanoapp that was loaded.
*/
@@ -195,7 +195,7 @@ public class ContextHubClientBroker extends IContextHubClient.Stub
}
/**
- * Handles a nanoapp unload event.
+ * Notifies the client of a nanoapp unload event if the connection is open.
*
* @param nanoAppId the ID of the nanoapp that was unloaded.
*/
@@ -211,7 +211,7 @@ public class ContextHubClientBroker extends IContextHubClient.Stub
}
/**
- * Handles a hub reset for this client.
+ * Notifies the client of a hub reset event if the connection is open.
*/
/* package */ void onHubReset() {
if (mConnectionOpen.get()) {
@@ -223,4 +223,21 @@ public class ContextHubClientBroker extends IContextHubClient.Stub
}
}
}
+
+ /**
+ * Notifies the client of a nanoapp abort event if the connection is open.
+ *
+ * @param nanoAppId the ID of the nanoapp that aborted
+ * @param abortCode the nanoapp specific abort code
+ */
+ /* package */ void onNanoAppAborted(long nanoAppId, int abortCode) {
+ if (mConnectionOpen.get()) {
+ try {
+ mCallbackInterface.onNanoAppAborted(nanoAppId, abortCode);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while calling onNanoAppAborted on client"
+ + " (host endpoint ID = " + mHostEndPointId + ")", e);
+ }
+ }
+ }
}
diff --git a/com/android/server/location/ContextHubClientManager.java b/com/android/server/location/ContextHubClientManager.java
index d58a7460..60b5b1f0 100644
--- a/com/android/server/location/ContextHubClientManager.java
+++ b/com/android/server/location/ContextHubClientManager.java
@@ -149,35 +149,38 @@ import java.util.function.Consumer;
}
/**
- * Handles a nanoapp load event.
- *
- * @param contextHubId the ID of the hub where the nanoapp was loaded.
- * @param nanoAppId the ID of the nanoapp that was loaded.
+ * @param contextHubId the ID of the hub where the nanoapp was loaded
+ * @param nanoAppId the ID of the nanoapp that was loaded
*/
/* package */ void onNanoAppLoaded(int contextHubId, long nanoAppId) {
forEachClientOfHub(contextHubId, client -> client.onNanoAppLoaded(nanoAppId));
}
/**
- * Handles a nanoapp unload event.
- *
- * @param contextHubId the ID of the hub where the nanoapp was unloaded.
- * @param nanoAppId the ID of the nanoapp that was unloaded.
+ * @param contextHubId the ID of the hub where the nanoapp was unloaded
+ * @param nanoAppId the ID of the nanoapp that was unloaded
*/
/* package */ void onNanoAppUnloaded(int contextHubId, long nanoAppId) {
forEachClientOfHub(contextHubId, client -> client.onNanoAppUnloaded(nanoAppId));
}
/**
- * Handles a hub reset.
- *
- * @param contextHubId the ID of the hub that has reset.
+ * @param contextHubId the ID of the hub that has reset
*/
/* package */ void onHubReset(int contextHubId) {
forEachClientOfHub(contextHubId, client -> client.onHubReset());
}
/**
+ * @param contextHubId the ID of the hub that contained the nanoapp that aborted
+ * @param nanoAppId the ID of the nanoapp that aborted
+ * @param abortCode the nanoapp specific abort code
+ */
+ /* package */ void onNanoAppAborted(int contextHubId, long nanoAppId, int abortCode) {
+ forEachClientOfHub(contextHubId, client -> client.onNanoAppAborted(nanoAppId, abortCode));
+ }
+
+ /**
* Creates a new ContextHubClientBroker object for a client and registers it with the client
* manager.
*
diff --git a/com/android/server/location/ContextHubService.java b/com/android/server/location/ContextHubService.java
index e08c6596..dc95d410 100644
--- a/com/android/server/location/ContextHubService.java
+++ b/com/android/server/location/ContextHubService.java
@@ -55,7 +55,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
-import java.util.concurrent.ConcurrentHashMap;
/**
* @hide
@@ -76,26 +75,12 @@ public class ContextHubService extends IContextHubService.Stub {
public static final int MSG_QUERY_MEMORY = 6;
public static final int MSG_HUB_RESET = 7;
- private static final String PRE_LOADED_GENERIC_UNKNOWN = "Preloaded app, unknown";
- private static final String PRE_LOADED_APP_NAME = PRE_LOADED_GENERIC_UNKNOWN;
- private static final String PRE_LOADED_APP_PUBLISHER = PRE_LOADED_GENERIC_UNKNOWN;
- private static final int PRE_LOADED_APP_MEM_REQ = 0;
-
private static final int OS_APP_INSTANCE = -1;
private final Context mContext;
- // TODO(b/69270990): Remove once old ContextHubManager API is deprecated
- // Service cache maintaining of instance ID to nanoapp infos
- private final ConcurrentHashMap<Integer, NanoAppInstanceInfo> mNanoAppHash =
- new ConcurrentHashMap<>();
- // The next available instance ID (managed by the service) to assign to a nanoapp
- private int mNextAvailableInstanceId = 0;
- // A map of the long nanoapp ID to instance ID managed by the service
- private final ConcurrentHashMap<Long, Integer> mNanoAppIdToInstanceMap =
- new ConcurrentHashMap<>();
-
- private final ContextHubInfo[] mContextHubInfo;
+ private final Map<Integer, ContextHubInfo> mContextHubIdToInfoMap;
+ private final List<ContextHubInfo> mContextHubInfoList;
private final RemoteCallbackList<IContextHubCallback> mCallbacksList =
new RemoteCallbackList<>();
@@ -111,6 +96,9 @@ public class ContextHubService extends IContextHubService.Stub {
// The default client for old API clients
private final Map<Integer, IContextHubClient> mDefaultClientMap;
+ // The manager for the internal nanoapp state cache
+ private final NanoAppStateManager mNanoAppStateManager = new NanoAppStateManager();
+
/**
* Class extending the callback to register with a Context Hub.
*/
@@ -154,13 +142,15 @@ public class ContextHubService extends IContextHubService.Stub {
if (mContextHubProxy == null) {
mTransactionManager = null;
mClientManager = null;
- mDefaultClientMap = Collections.EMPTY_MAP;
- mContextHubInfo = new ContextHubInfo[0];
+ mDefaultClientMap = Collections.emptyMap();
+ mContextHubIdToInfoMap = Collections.emptyMap();
+ mContextHubInfoList = Collections.emptyList();
return;
}
mClientManager = new ContextHubClientManager(mContext, mContextHubProxy);
- mTransactionManager = new ContextHubTransactionManager(mContextHubProxy, mClientManager);
+ mTransactionManager = new ContextHubTransactionManager(
+ mContextHubProxy, mClientManager, mNanoAppStateManager);
List<ContextHub> hubList;
try {
@@ -169,20 +159,16 @@ public class ContextHubService extends IContextHubService.Stub {
Log.e(TAG, "RemoteException while getting Context Hub info", e);
hubList = Collections.emptyList();
}
- mContextHubInfo = ContextHubServiceUtil.createContextHubInfoArray(hubList);
+ mContextHubIdToInfoMap = Collections.unmodifiableMap(
+ ContextHubServiceUtil.createContextHubInfoMap(hubList));
+ mContextHubInfoList = new ArrayList<>(mContextHubIdToInfoMap.values());
HashMap<Integer, IContextHubClient> defaultClientMap = new HashMap<>();
- for (ContextHubInfo contextHubInfo : mContextHubInfo) {
- int contextHubId = contextHubInfo.getId();
-
+ for (int contextHubId : mContextHubIdToInfoMap.keySet()) {
IContextHubClient client = mClientManager.registerClient(
createDefaultClientCallback(contextHubId), contextHubId);
defaultClientMap.put(contextHubId, client);
- }
- mDefaultClientMap = Collections.unmodifiableMap(defaultClientMap);
- for (ContextHubInfo contextHubInfo : mContextHubInfo) {
- int contextHubId = contextHubInfo.getId();
try {
mContextHubProxy.registerCallback(
contextHubId, new ContextHubServiceCallback(contextHubId));
@@ -190,18 +176,12 @@ public class ContextHubService extends IContextHubService.Stub {
Log.e(TAG, "RemoteException while registering service callback for hub (ID = "
+ contextHubId + ")", e);
}
- }
-
- // Do a query to initialize the service cache list of nanoapps
- // TODO(b/69270990): Remove this when old API is deprecated
- for (ContextHubInfo contextHubInfo : mContextHubInfo) {
- queryNanoAppsInternal(contextHubInfo.getId());
- }
- for (int i = 0; i < mContextHubInfo.length; i++) {
- Log.d(TAG, "ContextHub[" + i + "] id: " + mContextHubInfo[i].getId()
- + ", name: " + mContextHubInfo[i].getName());
+ // Do a query to initialize the service cache list of nanoapps
+ // TODO(b/69270990): Remove this when old API is deprecated
+ queryNanoAppsInternal(contextHubId);
}
+ mDefaultClientMap = Collections.unmodifiableMap(defaultClientMap);
}
/**
@@ -214,12 +194,11 @@ public class ContextHubService extends IContextHubService.Stub {
return new IContextHubClientCallback.Stub() {
@Override
public void onMessageFromNanoApp(NanoAppMessage message) {
- int nanoAppInstanceId =
- mNanoAppIdToInstanceMap.containsKey(message.getNanoAppId()) ?
- mNanoAppIdToInstanceMap.get(message.getNanoAppId()) : -1;
+ int nanoAppHandle = mNanoAppStateManager.getNanoAppHandle(
+ contextHubId, message.getNanoAppId());
onMessageReceiptOldApi(
- message.getMessageType(), contextHubId, nanoAppInstanceId,
+ message.getMessageType(), contextHubId, nanoAppHandle,
message.getMessageBody());
}
@@ -280,27 +259,29 @@ public class ContextHubService extends IContextHubService.Stub {
@Override
public int[] getContextHubHandles() throws RemoteException {
checkPermissions();
- int[] returnArray = new int[mContextHubInfo.length];
+ return ContextHubServiceUtil.createPrimitiveIntArray(mContextHubIdToInfoMap.keySet());
+ }
- Log.d(TAG, "System supports " + returnArray.length + " hubs");
- for (int i = 0; i < returnArray.length; ++i) {
- returnArray[i] = i;
- Log.d(TAG, String.format("Hub %s is mapped to %d",
- mContextHubInfo[i].getName(), returnArray[i]));
+ @Override
+ public ContextHubInfo getContextHubInfo(int contextHubHandle) throws RemoteException {
+ checkPermissions();
+ if (!mContextHubIdToInfoMap.containsKey(contextHubHandle)) {
+ Log.e(TAG, "Invalid Context Hub handle " + contextHubHandle + " in getContextHubInfo");
+ return null;
}
- return returnArray;
+ return mContextHubIdToInfoMap.get(contextHubHandle);
}
+ /**
+ * Returns a List of ContextHubInfo object describing the available hubs.
+ *
+ * @return the List of ContextHubInfo objects
+ */
@Override
- public ContextHubInfo getContextHubInfo(int contextHubId) throws RemoteException {
+ public List<ContextHubInfo> getContextHubs() throws RemoteException {
checkPermissions();
- if (!(contextHubId >= 0 && contextHubId < mContextHubInfo.length)) {
- Log.e(TAG, "Invalid context hub handle " + contextHubId);
- return null; // null means fail
- }
-
- return mContextHubInfo[contextHubId];
+ return mContextHubInfoList;
}
/**
@@ -328,15 +309,13 @@ public class ContextHubService extends IContextHubService.Stub {
* Creates an internal unload transaction callback to be used for old API clients
*
* @param contextHubId the ID of the hub to unload the nanoapp
- * @param nanoAppId the ID of the nanoapp to unload
* @return the callback interface
*/
- private IContextHubTransactionCallback createUnloadTransactionCallback(
- int contextHubId, long nanoAppId) {
+ private IContextHubTransactionCallback createUnloadTransactionCallback(int contextHubId) {
return new IContextHubTransactionCallback.Stub() {
@Override
public void onTransactionComplete(int result) {
- handleUnloadResponseOldApi(contextHubId, result, nanoAppId);
+ handleUnloadResponseOldApi(contextHubId, result);
}
@Override
@@ -365,112 +344,74 @@ public class ContextHubService extends IContextHubService.Stub {
};
}
- /**
- * Adds a new transaction to the transaction manager queue
- *
- * @param transaction the transaction to add
- * @return the result of adding the transaction
- */
- private int addTransaction(ContextHubServiceTransaction transaction) {
- int result = Result.OK;
- try {
- mTransactionManager.addTransaction(transaction);
- } catch (IllegalStateException e) {
- Log.e(TAG, e.getMessage());
- result = Result.TRANSACTION_PENDING; /* failed */
- }
-
- return result;
- }
-
@Override
- public int loadNanoApp(int contextHubId, NanoApp app) throws RemoteException {
+ public int loadNanoApp(int contextHubHandle, NanoApp nanoApp) throws RemoteException {
checkPermissions();
if (mContextHubProxy == null) {
return -1;
}
-
- if (!(contextHubId >= 0 && contextHubId < mContextHubInfo.length)) {
- Log.e(TAG, "Invalid contextHubhandle " + contextHubId);
+ if (!isValidContextHubId(contextHubHandle)) {
+ Log.e(TAG, "Invalid Context Hub handle " + contextHubHandle + " in loadNanoApp");
return -1;
}
- if (app == null) {
- Log.e(TAG, "Invalid null app");
+ if (nanoApp == null) {
+ Log.e(TAG, "NanoApp cannot be null in loadNanoApp");
return -1;
}
// Create an internal IContextHubTransactionCallback for the old API clients
- NanoAppBinary nanoAppBinary = new NanoAppBinary(app.getAppBinary());
+ NanoAppBinary nanoAppBinary = new NanoAppBinary(nanoApp.getAppBinary());
IContextHubTransactionCallback onCompleteCallback =
- createLoadTransactionCallback(contextHubId, nanoAppBinary);
+ createLoadTransactionCallback(contextHubHandle, nanoAppBinary);
ContextHubServiceTransaction transaction = mTransactionManager.createLoadTransaction(
- contextHubId, nanoAppBinary, onCompleteCallback);
+ contextHubHandle, nanoAppBinary, onCompleteCallback);
- int result = addTransaction(transaction);
- if (result != Result.OK) {
- Log.e(TAG, "Failed to load nanoapp with error code " + result);
- return -1;
- }
-
- // Do not add an entry to mNanoAppInstance Hash yet. The HAL may reject the app
+ mTransactionManager.addTransaction(transaction);
return 0;
}
@Override
- public int unloadNanoApp(int nanoAppInstanceHandle) throws RemoteException {
+ public int unloadNanoApp(int nanoAppHandle) throws RemoteException {
checkPermissions();
if (mContextHubProxy == null) {
return -1;
}
- NanoAppInstanceInfo info = mNanoAppHash.get(nanoAppInstanceHandle);
+ NanoAppInstanceInfo info =
+ mNanoAppStateManager.getNanoAppInstanceInfo(nanoAppHandle);
if (info == null) {
- Log.e(TAG, "Cannot find app with handle " + nanoAppInstanceHandle);
- return -1; //means failed
+ Log.e(TAG, "Invalid nanoapp handle " + nanoAppHandle + " in unloadNanoApp");
+ return -1;
}
int contextHubId = info.getContexthubId();
long nanoAppId = info.getAppId();
IContextHubTransactionCallback onCompleteCallback =
- createUnloadTransactionCallback(contextHubId, nanoAppId);
+ createUnloadTransactionCallback(contextHubId);
ContextHubServiceTransaction transaction = mTransactionManager.createUnloadTransaction(
contextHubId, nanoAppId, onCompleteCallback);
- int result = addTransaction(transaction);
- if (result != Result.OK) {
- Log.e(TAG, "Failed to unload nanoapp with error code " + result);
- return -1;
- }
-
- // Do not add an entry to mNanoAppInstance Hash yet. The HAL may reject the app
+ mTransactionManager.addTransaction(transaction);
return 0;
}
@Override
- public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppInstanceHandle)
- throws RemoteException {
+ public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) throws RemoteException {
checkPermissions();
- // This assumes that all the nanoAppInfo is current. This is reasonable
- // for the use cases for tightly controlled nanoApps.
- if (mNanoAppHash.containsKey(nanoAppInstanceHandle)) {
- return mNanoAppHash.get(nanoAppInstanceHandle);
- } else {
- Log.e(TAG, "Could not find nanoApp with handle " + nanoAppInstanceHandle);
- return null;
- }
+
+ return mNanoAppStateManager.getNanoAppInstanceInfo(nanoAppHandle);
}
@Override
- public int[] findNanoAppOnHub(int hubHandle, NanoAppFilter filter) throws RemoteException {
+ public int[] findNanoAppOnHub(
+ int contextHubHandle, NanoAppFilter filter) throws RemoteException {
checkPermissions();
- ArrayList<Integer> foundInstances = new ArrayList<Integer>();
-
- for (Integer nanoAppInstance : mNanoAppHash.keySet()) {
- NanoAppInstanceInfo info = mNanoAppHash.get(nanoAppInstance);
+ ArrayList<Integer> foundInstances = new ArrayList<>();
+ for (NanoAppInstanceInfo info : mNanoAppStateManager.getNanoAppInstanceInfoCollection()) {
if (filter.testMatch(info)) {
- foundInstances.add(nanoAppInstance);
+ foundInstances.add(info.getHandle());
}
}
@@ -478,8 +419,6 @@ public class ContextHubService extends IContextHubService.Stub {
for (int i = 0; i < foundInstances.size(); i++) {
retArray[i] = foundInstances.get(i).intValue();
}
-
- Log.w(TAG, "Found " + retArray.length + " apps on hub handle " + hubHandle);
return retArray;
}
@@ -491,6 +430,8 @@ public class ContextHubService extends IContextHubService.Stub {
*
* @param contextHubId the ID of the hub to do the query
* @return the result of the query
+ *
+ * @throws IllegalStateException if the transaction queue is full
*/
private int queryNanoAppsInternal(int contextHubId) {
if (mContextHubProxy == null) {
@@ -502,33 +443,34 @@ public class ContextHubService extends IContextHubService.Stub {
ContextHubServiceTransaction transaction = mTransactionManager.createQueryTransaction(
contextHubId, onCompleteCallback);
- return addTransaction(transaction);
+ mTransactionManager.addTransaction(transaction);
+ return Result.OK;
}
@Override
- public int sendMessage(
- int hubHandle, int nanoAppHandle, ContextHubMessage msg) throws RemoteException {
+ public int sendMessage(int contextHubHandle, int nanoAppHandle, ContextHubMessage msg)
+ throws RemoteException {
checkPermissions();
if (mContextHubProxy == null) {
return -1;
}
if (msg == null) {
- Log.e(TAG, "ContextHubMessage cannot be null");
+ Log.e(TAG, "ContextHubMessage cannot be null in sendMessage");
return -1;
}
if (msg.getData() == null) {
- Log.e(TAG, "ContextHubMessage message body cannot be null");
+ Log.e(TAG, "ContextHubMessage message body cannot be null in sendMessage");
return -1;
}
- if (!mDefaultClientMap.containsKey(hubHandle)) {
- Log.e(TAG, "Hub with ID " + hubHandle + " does not exist");
+ if (!isValidContextHubId(contextHubHandle)) {
+ Log.e(TAG, "Invalid Context Hub handle " + contextHubHandle + " in sendMessage");
return -1;
}
boolean success = false;
if (nanoAppHandle == OS_APP_INSTANCE) {
if (msg.getMsgType() == MSG_QUERY_NANO_APPS) {
- success = (queryNanoAppsInternal(hubHandle) == Result.OK);
+ success = (queryNanoAppsInternal(contextHubHandle) == Result.OK);
} else {
Log.e(TAG, "Invalid OS message params of type " + msg.getMsgType());
}
@@ -538,11 +480,11 @@ public class ContextHubService extends IContextHubService.Stub {
NanoAppMessage message = NanoAppMessage.createMessageToNanoApp(
info.getAppId(), msg.getMsgType(), msg.getData());
- IContextHubClient client = mDefaultClientMap.get(hubHandle);
+ IContextHubClient client = mDefaultClientMap.get(contextHubHandle);
success = (client.sendMessageToNanoApp(message) ==
- ContextHubTransaction.TRANSACTION_SUCCESS);
+ ContextHubTransaction.RESULT_SUCCESS);
} else {
- Log.e(TAG, "Failed to send nanoapp message - nanoapp with instance ID "
+ Log.e(TAG, "Failed to send nanoapp message - nanoapp with handle "
+ nanoAppHandle + " does not exist.");
}
}
@@ -572,26 +514,11 @@ public class ContextHubService extends IContextHubService.Stub {
return;
}
- // NOTE: The legacy JNI code used to do a query right after a load success
- // to synchronize the service cache. Instead store the binary that was requested to
- // load to update the cache later without doing a query.
- int instanceId = 0;
- long nanoAppId = nanoAppBinary.getNanoAppId();
- int nanoAppVersion = nanoAppBinary.getNanoAppVersion();
- if (result == TransactionResult.SUCCESS) {
- if (mNanoAppIdToInstanceMap.containsKey(nanoAppId)) {
- instanceId = mNanoAppIdToInstanceMap.get(nanoAppId);
- } else {
- instanceId = mNextAvailableInstanceId++;
- mNanoAppIdToInstanceMap.put(nanoAppId, instanceId);
- }
-
- addAppInstance(contextHubId, instanceId, nanoAppId, nanoAppVersion);
- }
-
byte[] data = new byte[5];
data[0] = (byte) result;
- ByteBuffer.wrap(data, 1, 4).order(ByteOrder.nativeOrder()).putInt(instanceId);
+ int nanoAppHandle = mNanoAppStateManager.getNanoAppHandle(
+ contextHubId, nanoAppBinary.getNanoAppId());
+ ByteBuffer.wrap(data, 1, 4).order(ByteOrder.nativeOrder()).putInt(nanoAppHandle);
onMessageReceiptOldApi(MSG_LOAD_NANO_APP, contextHubId, OS_APP_INSTANCE, data);
}
@@ -601,14 +528,7 @@ public class ContextHubService extends IContextHubService.Stub {
*
* TODO(b/69270990): Remove this once the old APIs are obsolete.
*/
- private void handleUnloadResponseOldApi(
- int contextHubId, int result, long nanoAppId) {
- if (result == TransactionResult.SUCCESS) {
- int instanceId = mNanoAppIdToInstanceMap.get(nanoAppId);
- deleteAppInstance(instanceId);
- mNanoAppIdToInstanceMap.remove(nanoAppId);
- }
-
+ private void handleUnloadResponseOldApi(int contextHubId, int result) {
byte[] data = new byte[1];
data[0] = (byte) result;
onMessageReceiptOldApi(MSG_UNLOAD_NANO_APP, contextHubId, OS_APP_INSTANCE, data);
@@ -651,7 +571,7 @@ public class ContextHubService extends IContextHubService.Stub {
* @param abortCode the nanoapp-specific abort code
*/
private void handleAppAbortCallback(int contextHubId, long nanoAppId, int abortCode) {
- // TODO(b/31049861): Implement this
+ mClientManager.onNanoAppAborted(contextHubId, nanoAppId, abortCode);
}
/**
@@ -664,53 +584,16 @@ public class ContextHubService extends IContextHubService.Stub {
List<NanoAppState> nanoAppStateList =
ContextHubServiceUtil.createNanoAppStateList(nanoAppInfoList);
- updateServiceCache(contextHubId, nanoAppInfoList);
+ mNanoAppStateManager.updateCache(contextHubId, nanoAppInfoList);
mTransactionManager.onQueryResponse(nanoAppStateList);
}
/**
- * Updates the service's cache of the list of loaded nanoapps using a nanoapp list response.
- *
- * TODO(b/69270990): Remove this when the old API functionality is removed.
- *
- * @param contextHubId the ID of the hub the response came from
- * @param nanoAppInfoList the list of loaded nanoapps
- */
- private void updateServiceCache(int contextHubId, List<HubAppInfo> nanoAppInfoList) {
- synchronized (mNanoAppHash) {
- for (int instanceId : mNanoAppHash.keySet()) {
- if (mNanoAppHash.get(instanceId).getContexthubId() == contextHubId) {
- deleteAppInstance(instanceId);
- }
- }
-
- for (HubAppInfo appInfo : nanoAppInfoList) {
- int instanceId;
- long nanoAppId = appInfo.appId;
- if (mNanoAppIdToInstanceMap.containsKey(nanoAppId)) {
- instanceId = mNanoAppIdToInstanceMap.get(nanoAppId);
- } else {
- instanceId = mNextAvailableInstanceId++;
- mNanoAppIdToInstanceMap.put(nanoAppId, instanceId);
- }
-
- addAppInstance(contextHubId, instanceId, nanoAppId, appInfo.version);
- }
- }
- }
-
- /**
* @param contextHubId the hub ID to validate
* @return {@code true} if the ID represents that of an available hub, {@code false} otherwise
*/
private boolean isValidContextHubId(int contextHubId) {
- for (ContextHubInfo hubInfo : mContextHubInfo) {
- if (hubInfo.getId() == contextHubId) {
- return true;
- }
- }
-
- return false;
+ return mContextHubIdToInfoMap.containsKey(contextHubId);
}
/**
@@ -738,6 +621,130 @@ public class ContextHubService extends IContextHubService.Stub {
return mClientManager.registerClient(clientCallback, contextHubId);
}
+ /**
+ * Loads a nanoapp binary at the specified Context hub.
+ *
+ * @param contextHubId the ID of the hub to load the binary
+ * @param transactionCallback the client-facing transaction callback interface
+ * @param nanoAppBinary the binary to load
+ *
+ * @throws IllegalStateException if the transaction queue is full
+ */
+ @Override
+ public void loadNanoAppOnHub(
+ int contextHubId, IContextHubTransactionCallback transactionCallback,
+ NanoAppBinary nanoAppBinary) throws RemoteException {
+ checkPermissions();
+ if (!checkHalProxyAndContextHubId(
+ contextHubId, transactionCallback, ContextHubTransaction.TYPE_LOAD_NANOAPP)) {
+ return;
+ }
+ if (nanoAppBinary == null) {
+ Log.e(TAG, "NanoAppBinary cannot be null in loadNanoAppOnHub");
+ transactionCallback.onTransactionComplete(
+ ContextHubTransaction.RESULT_FAILED_BAD_PARAMS);
+ return;
+ }
+
+ ContextHubServiceTransaction transaction = mTransactionManager.createLoadTransaction(
+ contextHubId, nanoAppBinary, transactionCallback);
+ mTransactionManager.addTransaction(transaction);
+ }
+
+ /**
+ * Unloads a nanoapp from the specified Context Hub.
+ *
+ * @param contextHubId the ID of the hub to unload the nanoapp
+ * @param transactionCallback the client-facing transaction callback interface
+ * @param nanoAppId the ID of the nanoapp to unload
+ *
+ * @throws IllegalStateException if the transaction queue is full
+ */
+ @Override
+ public void unloadNanoAppFromHub(
+ int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
+ throws RemoteException {
+ checkPermissions();
+ if (!checkHalProxyAndContextHubId(
+ contextHubId, transactionCallback, ContextHubTransaction.TYPE_UNLOAD_NANOAPP)) {
+ return;
+ }
+
+ ContextHubServiceTransaction transaction = mTransactionManager.createUnloadTransaction(
+ contextHubId, nanoAppId, transactionCallback);
+ mTransactionManager.addTransaction(transaction);
+ }
+
+ /**
+ * Enables a nanoapp at the specified Context Hub.
+ *
+ * @param contextHubId the ID of the hub to enable the nanoapp
+ * @param transactionCallback the client-facing transaction callback interface
+ * @param nanoAppId the ID of the nanoapp to enable
+ *
+ * @throws IllegalStateException if the transaction queue is full
+ */
+ @Override
+ public void enableNanoApp(
+ int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
+ throws RemoteException {
+ checkPermissions();
+ if (!checkHalProxyAndContextHubId(
+ contextHubId, transactionCallback, ContextHubTransaction.TYPE_ENABLE_NANOAPP)) {
+ return;
+ }
+
+ ContextHubServiceTransaction transaction = mTransactionManager.createEnableTransaction(
+ contextHubId, nanoAppId, transactionCallback);
+ mTransactionManager.addTransaction(transaction);
+ }
+
+ /**
+ * Disables a nanoapp at the specified Context Hub.
+ *
+ * @param contextHubId the ID of the hub to disable the nanoapp
+ * @param transactionCallback the client-facing transaction callback interface
+ * @param nanoAppId the ID of the nanoapp to disable
+ *
+ * @throws IllegalStateException if the transaction queue is full
+ */
+ @Override
+ public void disableNanoApp(
+ int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
+ throws RemoteException {
+ checkPermissions();
+ if (!checkHalProxyAndContextHubId(
+ contextHubId, transactionCallback, ContextHubTransaction.TYPE_DISABLE_NANOAPP)) {
+ return;
+ }
+
+ ContextHubServiceTransaction transaction = mTransactionManager.createDisableTransaction(
+ contextHubId, nanoAppId, transactionCallback);
+ mTransactionManager.addTransaction(transaction);
+ }
+
+ /**
+ * Queries for a list of nanoapps from the specified Context hub.
+ *
+ * @param contextHubId the ID of the hub to query
+ * @param transactionCallback the client-facing transaction callback interface
+ *
+ * @throws IllegalStateException if the transaction queue is full
+ */
+ @Override
+ public void queryNanoApps(int contextHubId, IContextHubTransactionCallback transactionCallback)
+ throws RemoteException {
+ checkPermissions();
+ if (!checkHalProxyAndContextHubId(
+ contextHubId, transactionCallback, ContextHubTransaction.TYPE_QUERY_NANOAPPS)) {
+ return;
+ }
+
+ ContextHubServiceTransaction transaction = mTransactionManager.createQueryTransaction(
+ contextHubId, transactionCallback);
+ mTransactionManager.addTransaction(transaction);
+ }
+
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -747,14 +754,14 @@ public class ContextHubService extends IContextHubService.Stub {
pw.println("");
// dump ContextHubInfo
pw.println("=================== CONTEXT HUBS ====================");
- for (int i = 0; i < mContextHubInfo.length; i++) {
- pw.println("Handle " + i + " : " + mContextHubInfo[i].toString());
+ for (ContextHubInfo hubInfo : mContextHubIdToInfoMap.values()) {
+ pw.println(hubInfo);
}
pw.println("");
pw.println("=================== NANOAPPS ====================");
// Dump nanoAppHash
- for (Integer nanoAppInstance : mNanoAppHash.keySet()) {
- pw.println(nanoAppInstance + " : " + mNanoAppHash.get(nanoAppInstance).toString());
+ for (NanoAppInstanceInfo info : mNanoAppStateManager.getNanoAppInstanceInfoCollection()) {
+ pw.println(info);
}
// dump eventLog
@@ -764,7 +771,8 @@ public class ContextHubService extends IContextHubService.Stub {
ContextHubServiceUtil.checkPermissions(mContext);
}
- private int onMessageReceiptOldApi(int msgType, int hubHandle, int appInstance, byte[] data) {
+ private int onMessageReceiptOldApi(
+ int msgType, int contextHubHandle, int appInstance, byte[] data) {
if (data == null) {
return -1;
}
@@ -772,7 +780,8 @@ public class ContextHubService extends IContextHubService.Stub {
int msgVersion = 0;
int callbacksCount = mCallbacksList.beginBroadcast();
Log.d(TAG, "Sending message " + msgType + " version " + msgVersion + " from hubHandle " +
- hubHandle + ", appInstance " + appInstance + ", callBackCount " + callbacksCount);
+ contextHubHandle + ", appInstance " + appInstance + ", callBackCount "
+ + callbacksCount);
if (callbacksCount < 1) {
Log.v(TAG, "No message callbacks registered.");
@@ -783,7 +792,7 @@ public class ContextHubService extends IContextHubService.Stub {
for (int i = 0; i < callbacksCount; ++i) {
IContextHubCallback callback = mCallbacksList.getBroadcastItem(i);
try {
- callback.onMessageReceipt(hubHandle, appInstance, msg);
+ callback.onMessageReceipt(contextHubHandle, appInstance, msg);
} catch (RemoteException e) {
Log.i(TAG, "Exception (" + e + ") calling remote callback (" + callback + ").");
continue;
@@ -793,39 +802,39 @@ public class ContextHubService extends IContextHubService.Stub {
return 0;
}
- private int addAppInstance(int hubHandle, int appInstanceHandle, long appId, int appVersion) {
- // App Id encodes vendor & version
- NanoAppInstanceInfo appInfo = new NanoAppInstanceInfo();
-
- appInfo.setAppId(appId);
- appInfo.setAppVersion(appVersion);
- appInfo.setName(PRE_LOADED_APP_NAME);
- appInfo.setContexthubId(hubHandle);
- appInfo.setHandle(appInstanceHandle);
- appInfo.setPublisher(PRE_LOADED_APP_PUBLISHER);
- appInfo.setNeededExecMemBytes(PRE_LOADED_APP_MEM_REQ);
- appInfo.setNeededReadMemBytes(PRE_LOADED_APP_MEM_REQ);
- appInfo.setNeededWriteMemBytes(PRE_LOADED_APP_MEM_REQ);
-
- String action;
- if (mNanoAppHash.containsKey(appInstanceHandle)) {
- action = "Updated";
- } else {
- action = "Added";
+ /**
+ * Validates the HAL proxy state and context hub ID to see if we can start the transaction.
+ *
+ * @param contextHubId the ID of the hub to start the transaction
+ * @param callback the client transaction callback interface
+ * @param transactionType the type of the transaction
+ *
+ * @return {@code true} if mContextHubProxy and contextHubId is valid, {@code false} otherwise
+ */
+ private boolean checkHalProxyAndContextHubId(
+ int contextHubId, IContextHubTransactionCallback callback,
+ @ContextHubTransaction.Type int transactionType) {
+ if (mContextHubProxy == null) {
+ try {
+ callback.onTransactionComplete(
+ ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while calling onTransactionComplete", e);
+ }
+ return false;
}
-
- mNanoAppHash.put(appInstanceHandle, appInfo);
- Log.d(TAG, action + " app instance " + appInstanceHandle + " with id 0x"
- + Long.toHexString(appId) + " version 0x" + Integer.toHexString(appVersion));
-
- return 0;
- }
-
- private int deleteAppInstance(int appInstanceHandle) {
- if (mNanoAppHash.remove(appInstanceHandle) == null) {
- return -1;
+ if (!isValidContextHubId(contextHubId)) {
+ Log.e(TAG, "Cannot start "
+ + ContextHubTransaction.typeToString(transactionType, false /* upperCase */)
+ + " transaction for invalid hub ID " + contextHubId);
+ try {
+ callback.onTransactionComplete(ContextHubTransaction.RESULT_FAILED_BAD_PARAMS);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while calling onTransactionComplete", e);
+ }
+ return false;
}
- return 0;
+ return true;
}
}
diff --git a/com/android/server/location/ContextHubServiceTransaction.java b/com/android/server/location/ContextHubServiceTransaction.java
index 66145bbb..c1fc9824 100644
--- a/com/android/server/location/ContextHubServiceTransaction.java
+++ b/com/android/server/location/ContextHubServiceTransaction.java
@@ -54,22 +54,14 @@ import java.util.concurrent.TimeUnit;
abstract int onTransact();
/**
- * A function to invoke when a transaction times out.
- *
- * All instances of this class must implement this method by reporting the timeout to the
- * client.
- */
- /* package */
- abstract void onTimeout();
-
- /**
* A function to invoke when the transaction completes.
*
- * Only relevant for load, unload, enable, or disable transactions.
+ * For transactions with expected contents (such as a query), the class instance should
+ * implement the appropriate behavior (e.g. invoke onQueryResponse with an empty list).
*
* @param result the result of the transaction
*/
- /* package */ void onTransactionComplete(int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
}
/**
@@ -80,7 +72,8 @@ import java.util.concurrent.TimeUnit;
* @param result the result of the query
* @param nanoAppStateList the list of nanoapps given by the query response
*/
- /* package */ void onQueryResponse(int result, List<NanoAppState> nanoAppStateList) {
+ /* package */ void onQueryResponse(
+ @ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) {
}
/**
@@ -134,28 +127,9 @@ import java.util.concurrent.TimeUnit;
return mIsComplete;
}
- /**
- * @return the human-readable string of this transaction's type
- */
- private String getTransactionTypeString() {
- switch (mTransactionType) {
- case ContextHubTransaction.TYPE_LOAD_NANOAPP:
- return "Load";
- case ContextHubTransaction.TYPE_UNLOAD_NANOAPP:
- return "Unload";
- case ContextHubTransaction.TYPE_ENABLE_NANOAPP:
- return "Enable";
- case ContextHubTransaction.TYPE_DISABLE_NANOAPP:
- return "Disable";
- case ContextHubTransaction.TYPE_QUERY_NANOAPPS:
- return "Query";
- default:
- return "Unknown";
- }
- }
-
@Override
public String toString() {
- return getTransactionTypeString() + " transaction (ID = " + mTransactionId + ")";
+ return ContextHubTransaction.typeToString(mTransactionType, true /* upperCase */)
+ + " transaction (ID = " + mTransactionId + ")";
}
}
diff --git a/com/android/server/location/ContextHubServiceUtil.java b/com/android/server/location/ContextHubServiceUtil.java
index 6faeb72e..c356b639 100644
--- a/com/android/server/location/ContextHubServiceUtil.java
+++ b/com/android/server/location/ContextHubServiceUtil.java
@@ -30,6 +30,9 @@ import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
import android.util.Log;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
@@ -43,19 +46,20 @@ import java.util.ArrayList;
+ HARDWARE_PERMISSION + "' not granted to access ContextHub Hardware";
/**
- * Creates a ContextHubInfo array from an ArrayList of HIDL ContextHub objects.
+ * Creates a ConcurrentHashMap of the Context Hub ID to the ContextHubInfo object given an
+ * ArrayList of HIDL ContextHub objects.
*
* @param hubList the ContextHub ArrayList
- * @return the ContextHubInfo array
+ * @return the HashMap object
*/
/* package */
- static ContextHubInfo[] createContextHubInfoArray(List<ContextHub> hubList) {
- ContextHubInfo[] contextHubInfoList = new ContextHubInfo[hubList.size()];
- for (int i = 0; i < hubList.size(); i++) {
- contextHubInfoList[i] = new ContextHubInfo(hubList.get(i));
+ static HashMap<Integer, ContextHubInfo> createContextHubInfoMap(List<ContextHub> hubList) {
+ HashMap<Integer, ContextHubInfo> contextHubIdToInfoMap = new HashMap<>();
+ for (ContextHub contextHub : hubList) {
+ contextHubIdToInfoMap.put(contextHub.hubId, new ContextHubInfo(contextHub));
}
- return contextHubInfoList;
+ return contextHubIdToInfoMap;
}
/**
@@ -90,6 +94,22 @@ import java.util.ArrayList;
}
/**
+ * Creates a primitive integer array given a Collection<Integer>.
+ * @param collection the collection to iterate
+ * @return the primitive integer array
+ */
+ static int[] createPrimitiveIntArray(Collection<Integer> collection) {
+ int[] primitiveArray = new int[collection.size()];
+
+ int i = 0;
+ for (int contextHubId : collection) {
+ primitiveArray[i++] = contextHubId;
+ }
+
+ return primitiveArray;
+ }
+
+ /**
* Generates the Context Hub HAL's NanoAppBinary object from the client-facing
* android.hardware.location.NanoAppBinary object.
*
@@ -195,17 +215,17 @@ import java.util.ArrayList;
static int toTransactionResult(int halResult) {
switch (halResult) {
case Result.OK:
- return ContextHubTransaction.TRANSACTION_SUCCESS;
+ return ContextHubTransaction.RESULT_SUCCESS;
case Result.BAD_PARAMS:
- return ContextHubTransaction.TRANSACTION_FAILED_BAD_PARAMS;
+ return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
case Result.NOT_INIT:
- return ContextHubTransaction.TRANSACTION_FAILED_UNINITIALIZED;
+ return ContextHubTransaction.RESULT_FAILED_UNINITIALIZED;
case Result.TRANSACTION_PENDING:
- return ContextHubTransaction.TRANSACTION_FAILED_PENDING;
+ return ContextHubTransaction.RESULT_FAILED_PENDING;
case Result.TRANSACTION_FAILED:
case Result.UNKNOWN_FAILURE:
default: /* fall through */
- return ContextHubTransaction.TRANSACTION_FAILED_UNKNOWN;
+ return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
}
diff --git a/com/android/server/location/ContextHubTransactionManager.java b/com/android/server/location/ContextHubTransactionManager.java
index 47d9d568..cced781f 100644
--- a/com/android/server/location/ContextHubTransactionManager.java
+++ b/com/android/server/location/ContextHubTransactionManager.java
@@ -50,7 +50,7 @@ import java.util.concurrent.atomic.AtomicInteger;
/*
* Maximum number of transaction requests that can be pending at a time
*/
- private static final int MAX_PENDING_REQUESTS = 10;
+ private static final int MAX_PENDING_REQUESTS = 10000;
/*
* The proxy to talk to the Context Hub
@@ -63,6 +63,11 @@ import java.util.concurrent.atomic.AtomicInteger;
private final ContextHubClientManager mClientManager;
/*
+ * The nanoapp state manager for the service
+ */
+ private final NanoAppStateManager mNanoAppStateManager;
+
+ /*
* A queue containing the current transactions
*/
private final ArrayDeque<ContextHubServiceTransaction> mTransactionQueue = new ArrayDeque<>();
@@ -79,9 +84,11 @@ import java.util.concurrent.atomic.AtomicInteger;
private ScheduledFuture<?> mTimeoutFuture = null;
/* package */ ContextHubTransactionManager(
- IContexthub contextHubProxy, ContextHubClientManager clientManager) {
+ IContexthub contextHubProxy, ContextHubClientManager clientManager,
+ NanoAppStateManager nanoAppStateManager) {
mContextHubProxy = contextHubProxy;
mClientManager = clientManager;
+ mNanoAppStateManager = nanoAppStateManager;
}
/**
@@ -106,25 +113,28 @@ import java.util.concurrent.atomic.AtomicInteger;
contextHubId, hidlNanoAppBinary, this.getTransactionId());
} catch (RemoteException e) {
Log.e(TAG, "RemoteException while trying to load nanoapp with ID 0x" +
- Long.toHexString(nanoAppBinary.getNanoAppId()));
+ Long.toHexString(nanoAppBinary.getNanoAppId()), e);
return Result.UNKNOWN_FAILURE;
}
}
@Override
- /* package */ void onTimeout() {
- onTransactionComplete(ContextHubTransaction.TRANSACTION_FAILED_TIMEOUT);
- }
-
- @Override
- /* package */ void onTransactionComplete(int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ if (result == ContextHubTransaction.RESULT_SUCCESS) {
+ // NOTE: The legacy JNI code used to do a query right after a load success
+ // to synchronize the service cache. Instead store the binary that was
+ // requested to load to update the cache later without doing a query.
+ mNanoAppStateManager.addNanoAppInstance(
+ contextHubId, nanoAppBinary.getNanoAppId(),
+ nanoAppBinary.getNanoAppVersion());
+ }
try {
onCompleteCallback.onTransactionComplete(result);
- if (result == Result.OK) {
+ if (result == ContextHubTransaction.RESULT_SUCCESS) {
mClientManager.onNanoAppLoaded(contextHubId, nanoAppBinary.getNanoAppId());
}
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while calling client onTransactionComplete");
+ Log.e(TAG, "RemoteException while calling client onTransactionComplete", e);
}
}
};
@@ -133,7 +143,7 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Creates a transaction for unloading a nanoapp.
*
- * @param contextHubId the ID of the hub to load the nanoapp to
+ * @param contextHubId the ID of the hub to unload the nanoapp from
* @param nanoAppId the ID of the nanoapp to unload
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
@@ -149,25 +159,93 @@ import java.util.concurrent.atomic.AtomicInteger;
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
Log.e(TAG, "RemoteException while trying to unload nanoapp with ID 0x" +
- Long.toHexString(nanoAppId));
+ Long.toHexString(nanoAppId), e);
+ return Result.UNKNOWN_FAILURE;
+ }
+ }
+
+ @Override
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ if (result == ContextHubTransaction.RESULT_SUCCESS) {
+ mNanoAppStateManager.removeNanoAppInstance(contextHubId, nanoAppId);
+ }
+ try {
+ onCompleteCallback.onTransactionComplete(result);
+ if (result == ContextHubTransaction.RESULT_SUCCESS) {
+ mClientManager.onNanoAppUnloaded(contextHubId, nanoAppId);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while calling client onTransactionComplete", e);
+ }
+ }
+ };
+ }
+
+ /**
+ * Creates a transaction for enabling a nanoapp.
+ *
+ * @param contextHubId the ID of the hub to enable the nanoapp on
+ * @param nanoAppId the ID of the nanoapp to enable
+ * @param onCompleteCallback the client on complete callback
+ * @return the generated transaction
+ */
+ /* package */ ContextHubServiceTransaction createEnableTransaction(
+ int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback) {
+ return new ContextHubServiceTransaction(
+ mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_ENABLE_NANOAPP) {
+ @Override
+ /* package */ int onTransact() {
+ try {
+ return mContextHubProxy.enableNanoApp(
+ contextHubId, nanoAppId, this.getTransactionId());
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while trying to enable nanoapp with ID 0x" +
+ Long.toHexString(nanoAppId), e);
return Result.UNKNOWN_FAILURE;
}
}
@Override
- /* package */ void onTimeout() {
- onTransactionComplete(ContextHubTransaction.TRANSACTION_FAILED_TIMEOUT);
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ try {
+ onCompleteCallback.onTransactionComplete(result);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while calling client onTransactionComplete", e);
+ }
+ }
+ };
+ }
+
+ /**
+ * Creates a transaction for disabling a nanoapp.
+ *
+ * @param contextHubId the ID of the hub to disable the nanoapp on
+ * @param nanoAppId the ID of the nanoapp to disable
+ * @param onCompleteCallback the client on complete callback
+ * @return the generated transaction
+ */
+ /* package */ ContextHubServiceTransaction createDisableTransaction(
+ int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback) {
+ return new ContextHubServiceTransaction(
+ mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_DISABLE_NANOAPP) {
+ @Override
+ /* package */ int onTransact() {
+ try {
+ return mContextHubProxy.disableNanoApp(
+ contextHubId, nanoAppId, this.getTransactionId());
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while trying to disable nanoapp with ID 0x" +
+ Long.toHexString(nanoAppId), e);
+ return Result.UNKNOWN_FAILURE;
+ }
}
@Override
- /* package */ void onTransactionComplete(int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
try {
onCompleteCallback.onTransactionComplete(result);
- if (result == Result.OK) {
- mClientManager.onNanoAppUnloaded(contextHubId, nanoAppId);
- }
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while calling client onTransactionComplete");
+ Log.e(TAG, "RemoteException while calling client onTransactionComplete", e);
}
}
};
@@ -189,23 +267,23 @@ import java.util.concurrent.atomic.AtomicInteger;
try {
return mContextHubProxy.queryApps(contextHubId);
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to query for nanoapps");
+ Log.e(TAG, "RemoteException while trying to query for nanoapps", e);
return Result.UNKNOWN_FAILURE;
}
}
@Override
- /* package */ void onTimeout() {
- onQueryResponse(ContextHubTransaction.TRANSACTION_FAILED_TIMEOUT,
- Collections.emptyList());
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ onQueryResponse(result, Collections.emptyList());
}
@Override
- /* package */ void onQueryResponse(int result, List<NanoAppState> nanoAppStateList) {
+ /* package */ void onQueryResponse(
+ @ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) {
try {
onCompleteCallback.onQueryResponse(result, nanoAppStateList);
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while calling client onQueryComplete");
+ Log.e(TAG, "RemoteException while calling client onQueryComplete", e);
}
}
};
@@ -215,7 +293,8 @@ import java.util.concurrent.atomic.AtomicInteger;
* Adds a new transaction to the queue.
*
* If there was no pending transaction at the time, the transaction that was added will be
- * started in this method.
+ * started in this method. If there were too many transactions in the queue, an exception will
+ * be thrown.
*
* @param transaction the transaction to add
* @throws IllegalStateException if the queue is full
@@ -224,7 +303,7 @@ import java.util.concurrent.atomic.AtomicInteger;
synchronized void addTransaction(
ContextHubServiceTransaction transaction) throws IllegalStateException {
if (mTransactionQueue.size() == MAX_PENDING_REQUESTS) {
- throw new IllegalStateException("Transaction transaction queue is full (capacity = "
+ throw new IllegalStateException("Transaction queue is full (capacity = "
+ MAX_PENDING_REQUESTS + ")");
}
mTransactionQueue.add(transaction);
@@ -238,7 +317,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* Handles a transaction response from a Context Hub.
*
* @param transactionId the transaction ID of the response
- * @param result the result of the transaction
+ * @param result the result of the transaction as defined by the HAL TransactionResult
*/
/* package */
synchronized void onTransactionResponse(int transactionId, int result) {
@@ -253,7 +332,10 @@ import java.util.concurrent.atomic.AtomicInteger;
return;
}
- transaction.onTransactionComplete(result);
+ transaction.onTransactionComplete(
+ (result == TransactionResult.SUCCESS) ?
+ ContextHubTransaction.RESULT_SUCCESS :
+ ContextHubTransaction.RESULT_FAILED_AT_HUB);
removeTransactionAndStartNext();
}
@@ -274,7 +356,7 @@ import java.util.concurrent.atomic.AtomicInteger;
return;
}
- transaction.onQueryResponse(TransactionResult.SUCCESS, nanoAppStateList);
+ transaction.onQueryResponse(ContextHubTransaction.RESULT_SUCCESS, nanoAppStateList);
removeTransactionAndStartNext();
}
@@ -333,7 +415,8 @@ import java.util.concurrent.atomic.AtomicInteger;
synchronized (this) {
if (!transaction.isComplete()) {
Log.d(TAG, transaction + " timed out");
- transaction.onTimeout();
+ transaction.onTransactionComplete(
+ ContextHubTransaction.RESULT_FAILED_TIMEOUT);
removeTransactionAndStartNext();
}
@@ -344,6 +427,8 @@ import java.util.concurrent.atomic.AtomicInteger;
mTimeoutFuture = mTimeoutExecutor.schedule(onTimeoutFunc, timeoutSeconds,
TimeUnit.SECONDS);
} else {
+ transaction.onTransactionComplete(
+ ContextHubServiceUtil.toTransactionResult(result));
mTransactionQueue.remove();
}
}
diff --git a/com/android/server/location/GnssLocationProvider.java b/com/android/server/location/GnssLocationProvider.java
index 0b511f23..e6de07d2 100644
--- a/com/android/server/location/GnssLocationProvider.java
+++ b/com/android/server/location/GnssLocationProvider.java
@@ -233,13 +233,13 @@ public class GnssLocationProvider implements LocationProviderInterface {
private static final int AGPS_SETID_TYPE_IMSI = 1;
private static final int AGPS_SETID_TYPE_MSISDN = 2;
- private static final int GPS_GEOFENCE_UNAVAILABLE = 1<<0L;
- private static final int GPS_GEOFENCE_AVAILABLE = 1<<1L;
+ private static final int GPS_GEOFENCE_UNAVAILABLE = 1 << 0L;
+ private static final int GPS_GEOFENCE_AVAILABLE = 1 << 1L;
// GPS Geofence errors. Should match GeofenceStatus enum in IGnssGeofenceCallback.hal.
private static final int GPS_GEOFENCE_OPERATION_SUCCESS = 0;
private static final int GPS_GEOFENCE_ERROR_TOO_MANY_GEOFENCES = 100;
- private static final int GPS_GEOFENCE_ERROR_ID_EXISTS = -101;
+ private static final int GPS_GEOFENCE_ERROR_ID_EXISTS = -101;
private static final int GPS_GEOFENCE_ERROR_ID_UNKNOWN = -102;
private static final int GPS_GEOFENCE_ERROR_INVALID_TRANSITION = -103;
private static final int GPS_GEOFENCE_ERROR_GENERIC = -149;
@@ -253,6 +253,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
private static class GpsRequest {
public ProviderRequest request;
public WorkSource source;
+
public GpsRequest(ProviderRequest request, WorkSource source) {
this.request = request;
this.source = source;
@@ -280,15 +281,15 @@ public class GnssLocationProvider implements LocationProviderInterface {
// how often to request NTP time, in milliseconds
// current setting 24 hours
- private static final long NTP_INTERVAL = 24*60*60*1000;
+ private static final long NTP_INTERVAL = 24 * 60 * 60 * 1000;
// how long to wait if we have a network error in NTP or XTRA downloading
// the initial value of the exponential backoff
// current setting - 5 minutes
- private static final long RETRY_INTERVAL = 5*60*1000;
+ private static final long RETRY_INTERVAL = 5 * 60 * 1000;
// how long to wait if we have a network error in NTP or XTRA downloading
// the max value of the exponential backoff
// current setting - 4 hours
- private static final long MAX_RETRY_INTERVAL = 4*60*60*1000;
+ private static final long MAX_RETRY_INTERVAL = 4 * 60 * 60 * 1000;
// Timeout when holding wakelocks for downloading XTRA data.
private static final long DOWNLOAD_XTRA_DATA_TIMEOUT_MS = 60 * 1000;
@@ -321,6 +322,9 @@ public class GnssLocationProvider implements LocationProviderInterface {
// requested frequency of fixes, in milliseconds
private int mFixInterval = 1000;
+ // true if low power mode for the GNSS chipset is part of the latest request.
+ private boolean mLowPowerMode = false;
+
// true if we started navigation
private boolean mStarted;
@@ -398,7 +402,6 @@ public class GnssLocationProvider implements LocationProviderInterface {
private final static String LPP_PROFILE = "persist.sys.gps.lpp";
-
private final PowerManager mPowerManager;
private final AlarmManager mAlarmManager;
private final PendingIntent mWakeupIntent;
@@ -411,16 +414,16 @@ public class GnssLocationProvider implements LocationProviderInterface {
private WorkSource mClientSource = new WorkSource();
private GeofenceHardwareImpl mGeofenceHardwareImpl;
- private int mYearOfHardware = 0;
+
+ // Volatile for simple inter-thread sync on these values.
+ private volatile int mHardwareYear = 0;
+ private volatile String mHardwareModelName = LocationManager.GNSS_HARDWARE_MODEL_NAME_UNKNOWN;
// Set lower than the current ITAR limit of 600m/s to allow this to trigger even if GPS HAL
// stops output right at 600m/s, depriving this of the information of a device that reaches
// greater than 600m/s, and higher than the speed of sound to avoid impacting most use cases.
private static final float ITAR_SPEED_LIMIT_METERS_PER_SECOND = 400.0F;
- // TODO: improve comment
- // Volatile to ensure that potentially near-concurrent outputs from HAL
- // react to this value change promptly
private volatile boolean mItarSpeedLimitExceeded = false;
// GNSS Metrics
@@ -459,26 +462,27 @@ public class GnssLocationProvider implements LocationProviderInterface {
*/
private final ConnectivityManager.NetworkCallback mNetworkConnectivityCallback =
new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(Network network) {
- if (mInjectNtpTimePending == STATE_PENDING_NETWORK) {
- requestUtcTime();
- }
- if (mDownloadXtraDataPending == STATE_PENDING_NETWORK) {
- if (mSupportsXtra) {
- // Download only if supported, (prevents an unneccesary on-boot download)
- xtraDownloadRequest();
+ @Override
+ public void onAvailable(Network network) {
+ if (mInjectNtpTimePending == STATE_PENDING_NETWORK) {
+ requestUtcTime();
+ }
+ if (mDownloadXtraDataPending == STATE_PENDING_NETWORK) {
+ if (mSupportsXtra) {
+ // Download only if supported, (prevents an unneccesary on-boot
+ // download)
+ xtraDownloadRequest();
+ }
+ }
+ // Always on, notify HAL so it can get data it needs
+ sendMessage(UPDATE_NETWORK_STATE, 0 /*arg*/, network);
}
- }
- // Always on, notify HAL so it can get data it needs
- sendMessage(UPDATE_NETWORK_STATE, 0 /*arg*/, network);
- }
- @Override
- public void onLost(Network network) {
- sendMessage(UPDATE_NETWORK_STATE, 0 /*arg*/, network);
- }
- };
+ @Override
+ public void onLost(Network network) {
+ sendMessage(UPDATE_NETWORK_STATE, 0 /*arg*/, network);
+ }
+ };
/**
* Callback used to listen for availability of a requested SUPL connection.
@@ -487,20 +491,21 @@ public class GnssLocationProvider implements LocationProviderInterface {
*/
private final ConnectivityManager.NetworkCallback mSuplConnectivityCallback =
new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(Network network) {
- // Specific to a change to a SUPL enabled network becoming ready
- sendMessage(UPDATE_NETWORK_STATE, 0 /*arg*/, network);
- }
+ @Override
+ public void onAvailable(Network network) {
+ // Specific to a change to a SUPL enabled network becoming ready
+ sendMessage(UPDATE_NETWORK_STATE, 0 /*arg*/, network);
+ }
- @Override
- public void onLost(Network network) {
- releaseSuplConnection(GPS_RELEASE_AGPS_DATA_CONN);
- }
- };
+ @Override
+ public void onLost(Network network) {
+ releaseSuplConnection(GPS_RELEASE_AGPS_DATA_CONN);
+ }
+ };
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
- @Override public void onReceive(Context context, Intent intent) {
+ @Override
+ public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DEBUG) Log.d(TAG, "receive broadcast intent, action: " + action);
if (action == null) {
@@ -524,11 +529,11 @@ public class GnssLocationProvider implements LocationProviderInterface {
private final OnSubscriptionsChangedListener mOnSubscriptionsChangedListener =
new OnSubscriptionsChangedListener() {
- @Override
- public void onSubscriptionsChanged() {
- sendMessage(SUBSCRIPTION_OR_SIM_CHANGED, 0, null);
- }
- };
+ @Override
+ public void onSubscriptionsChanged() {
+ sendMessage(SUBSCRIPTION_OR_SIM_CHANGED, 0, null);
+ }
+ };
private void subscriptionOrSimChanged(Context context) {
if (DEBUG) Log.d(TAG, "received SIM related action: ");
@@ -599,8 +604,8 @@ public class GnssLocationProvider implements LocationProviderInterface {
String lpp_prof = SystemProperties.get(LPP_PROFILE);
if (!TextUtils.isEmpty(lpp_prof)) {
- // override default value of this if lpp_prof is not empty
- properties.setProperty("LPP_PROFILE", lpp_prof);
+ // override default value of this if lpp_prof is not empty
+ properties.setProperty("LPP_PROFILE", lpp_prof);
}
/*
* Overlay carrier properties from a debug configuration file.
@@ -608,7 +613,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
loadPropertiesFromFile(DEBUG_PROPERTIES_FILE, properties);
// TODO: we should get rid of C2K specific setting.
setSuplHostPort(properties.getProperty("SUPL_HOST"),
- properties.getProperty("SUPL_PORT"));
+ properties.getProperty("SUPL_PORT"));
mC2KServerHost = properties.getProperty("C2K_HOST");
String portString = properties.getProperty("C2K_PORT");
if (mC2KServerHost != null && portString != null) {
@@ -625,24 +630,26 @@ public class GnssLocationProvider implements LocationProviderInterface {
put("SUPL_MODE", (val) -> native_set_supl_mode(val));
put("SUPL_ES", (val) -> native_set_supl_es(val));
put("LPP_PROFILE", (val) -> native_set_lpp_profile(val));
- put("A_GLONASS_POS_PROTOCOL_SELECT", (val) -> native_set_gnss_pos_protocol_select(val));
- put("USE_EMERGENCY_PDN_FOR_EMERGENCY_SUPL", (val) -> native_set_emergency_supl_pdn(val));
+ put("A_GLONASS_POS_PROTOCOL_SELECT",
+ (val) -> native_set_gnss_pos_protocol_select(val));
+ put("USE_EMERGENCY_PDN_FOR_EMERGENCY_SUPL",
+ (val) -> native_set_emergency_supl_pdn(val));
put("GPS_LOCK", (val) -> native_set_gps_lock(val));
}
};
- for(Entry<String, SetCarrierProperty> entry : map.entrySet()) {
+ for (Entry<String, SetCarrierProperty> entry : map.entrySet()) {
String propertyName = entry.getKey();
String propertyValueString = properties.getProperty(propertyName);
if (propertyValueString != null) {
try {
- int propertyValueInt = Integer.decode(propertyValueString);
- boolean result = entry.getValue().set(propertyValueInt);
- if (result == false) {
- Log.e(TAG, "Unable to set " + propertyName);
- }
+ int propertyValueInt = Integer.decode(propertyValueString);
+ boolean result = entry.getValue().set(propertyValueInt);
+ if (result == false) {
+ Log.e(TAG, "Unable to set " + propertyName);
+ }
} catch (NumberFormatException e) {
- Log.e(TAG, "unable to parse propertyName: " + propertyValueString);
+ Log.e(TAG, "unable to parse propertyName: " + propertyValueString);
}
}
}
@@ -663,7 +670,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
private void loadPropertiesFromResource(Context context,
- Properties properties) {
+ Properties properties) {
String[] configValues = context.getResources().getStringArray(
com.android.internal.R.array.config_gpsParameters);
for (String item : configValues) {
@@ -681,7 +688,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
private boolean loadPropertiesFromFile(String filename,
- Properties properties) {
+ Properties properties) {
try {
File file = new File(filename);
FileInputStream stream = null;
@@ -717,11 +724,11 @@ public class GnssLocationProvider implements LocationProviderInterface {
PowerManager.PARTIAL_WAKE_LOCK, DOWNLOAD_EXTRA_WAKELOCK_KEY);
mDownloadXtraWakeLock.setReferenceCounted(true);
- mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
+ mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
mWakeupIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ALARM_WAKEUP), 0);
mTimeoutIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ALARM_TIMEOUT), 0);
- mConnMgr = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mConnMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
// App ops service to keep track of who is accessing the GPS
mAppOpsService = IAppOpsService.Stub.asInterface(ServiceManager.getService(
@@ -744,8 +751,8 @@ public class GnssLocationProvider implements LocationProviderInterface {
// Create a GPS net-initiated handler.
mNIHandler = new GpsNetInitiatedHandler(context,
- mNetInitiatedListener,
- mSuplEsEnabled);
+ mNetInitiatedListener,
+ mSuplEsEnabled);
mListenerHelper = new GnssStatusListenerHelper(mHandler) {
@Override
@@ -766,8 +773,23 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
@Override
- protected boolean registerWithService() {
- return native_start_measurement_collection();
+ protected int registerWithService() {
+ int devOptions = Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Global.DEVELOPMENT_SETTINGS_ENABLED , 0);
+ int fullTrackingToggled = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.ENABLE_GNSS_RAW_MEAS_FULL_TRACKING , 0);
+ boolean result = false;
+ if (devOptions == 1 /* Developer Mode enabled */
+ && fullTrackingToggled == 1 /* Raw Measurements Full Tracking enabled */) {
+ result = native_start_measurement_collection(true /* enableFullTracking */);
+ } else {
+ result = native_start_measurement_collection(false /* enableFullTracking */);
+ }
+ if (result) {
+ return RemoteListenerHelper.RESULT_SUCCESS;
+ } else {
+ return RemoteListenerHelper.RESULT_INTERNAL_ERROR;
+ }
}
@Override
@@ -788,8 +810,13 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
@Override
- protected boolean registerWithService() {
- return native_start_navigation_message_collection();
+ protected int registerWithService() {
+ boolean result = native_start_navigation_message_collection();
+ if (result) {
+ return RemoteListenerHelper.RESULT_SUCCESS;
+ } else {
+ return RemoteListenerHelper.RESULT_INTERNAL_ERROR;
+ }
}
@Override
@@ -1116,9 +1143,9 @@ public class GnssLocationProvider implements LocationProviderInterface {
* Checks what SUPL mode to use, according to the AGPS mode as well as the
* allowed mode from properties.
*
- * @param properties GPS properties
+ * @param properties GPS properties
* @param agpsEnabled whether AGPS is enabled by settings value
- * @param singleShot whether "singleshot" is needed
+ * @param singleShot whether "singleshot" is needed
* @return SUPL mode (MSA vs MSB vs STANDALONE)
*/
private int getSuplMode(Properties properties, boolean agpsEnabled, boolean singleShot) {
@@ -1222,7 +1249,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
private void updateStatus(int status, int svCount, int meanCn0, int maxCn0) {
- if (status != mStatus || svCount != mSvCount || meanCn0 != mMeanCn0 || maxCn0 != mMaxCn0 ) {
+ if (status != mStatus || svCount != mSvCount || meanCn0 != mMeanCn0 || maxCn0 != mMaxCn0) {
mStatus = status;
mSvCount = svCount;
mMeanCn0 = meanCn0;
@@ -1284,7 +1311,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
updateClientUids(mWorkSource);
mFixInterval = (int) mProviderRequest.interval;
-
+ mLowPowerMode = (boolean) mProviderRequest.lowPowerMode;
// check for overflow
if (mFixInterval != mProviderRequest.interval) {
Log.w(TAG, "interval overflow: " + mProviderRequest.interval);
@@ -1293,14 +1320,22 @@ public class GnssLocationProvider implements LocationProviderInterface {
// apply request to GPS engine
if (mStarted && hasCapability(GPS_CAPABILITY_SCHEDULING)) {
- // change period
+ // change period and/or lowPowerMode
if (!native_set_position_mode(mPositionMode, GPS_POSITION_RECURRENCE_PERIODIC,
- mFixInterval, 0, 0)) {
- Log.e(TAG, "set_position_mode failed in setMinTime()");
+ mFixInterval, 0, 0, mLowPowerMode)) {
+ Log.e(TAG, "set_position_mode failed in updateRequirements");
}
} else if (!mStarted) {
// start GPS
startNavigating(singleShot);
+ } else {
+ // GNSS Engine is already ON, but no GPS_CAPABILITY_SCHEDULING
+ mAlarmManager.cancel(mTimeoutIntent);
+ if (mFixInterval >= NO_FIX_TIMEOUT) {
+ // set timer to give up if we do not receive a fix within NO_FIX_TIMEOUT
+ // and our fix interval is not short
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + NO_FIX_TIMEOUT, mTimeoutIntent); }
}
} else {
updateClientUids(new WorkSource());
@@ -1323,7 +1358,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
// Update sources that were not previously tracked.
if (newWork != null) {
int lastuid = -1;
- for (int i=0; i<newWork.size(); i++) {
+ for (int i = 0; i < newWork.size(); i++) {
try {
int uid = newWork.get(i);
mAppOpsService.startOperation(AppOpsManager.getToken(mAppOpsService),
@@ -1341,7 +1376,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
// Update sources that are no longer tracked.
if (goneWork != null) {
int lastuid = -1;
- for (int i=0; i<goneWork.size(); i++) {
+ for (int i = 0; i < goneWork.size(); i++) {
try {
int uid = goneWork.get(i);
mAppOpsService.finishOperation(AppOpsManager.getToken(mAppOpsService),
@@ -1455,13 +1490,13 @@ public class GnssLocationProvider implements LocationProviderInterface {
boolean agpsEnabled =
(Settings.Global.getInt(mContext.getContentResolver(),
- Settings.Global.ASSISTED_GPS_ENABLED, 1) != 0);
+ Settings.Global.ASSISTED_GPS_ENABLED, 1) != 0);
mPositionMode = getSuplMode(mProperties, agpsEnabled, singleShot);
if (DEBUG) {
String mode;
- switch(mPositionMode) {
+ switch (mPositionMode) {
case GPS_POSITION_MODE_STANDALONE:
mode = "standalone";
break;
@@ -1479,8 +1514,9 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
int interval = (hasCapability(GPS_CAPABILITY_SCHEDULING) ? mFixInterval : 1000);
+ mLowPowerMode = (boolean) mProviderRequest.lowPowerMode;
if (!native_set_position_mode(mPositionMode, GPS_POSITION_RECURRENCE_PERIODIC,
- interval, 0, 0)) {
+ interval, 0, 0, mLowPowerMode)) {
mStarted = false;
Log.e(TAG, "set_position_mode failed in startNavigating()");
return;
@@ -1578,7 +1614,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
mLastFixTime = SystemClock.elapsedRealtime();
// report time to first fix
if (mTimeToFirstFix == 0 && hasLatLong) {
- mTimeToFirstFix = (int)(mLastFixTime - mFixRequestTime);
+ mTimeToFirstFix = (int) (mLastFixTime - mFixRequestTime);
if (DEBUG) Log.d(TAG, "TTFF: " + mTimeToFirstFix);
mGnssMetrics.logTimeToFirstFixMilliSecs(mTimeToFirstFix);
@@ -1604,12 +1640,12 @@ public class GnssLocationProvider implements LocationProviderInterface {
updateStatus(LocationProvider.AVAILABLE, mSvCount, mMeanCn0, mMaxCn0);
}
- if (!hasCapability(GPS_CAPABILITY_SCHEDULING) && mStarted &&
- mFixInterval > GPS_POLLING_THRESHOLD_INTERVAL) {
+ if (!hasCapability(GPS_CAPABILITY_SCHEDULING) && mStarted &&
+ mFixInterval > GPS_POLLING_THRESHOLD_INTERVAL) {
if (DEBUG) Log.d(TAG, "got fix, hibernating");
hibernate();
}
- }
+ }
/**
* called from native code to update our status
@@ -1650,10 +1686,10 @@ public class GnssLocationProvider implements LocationProviderInterface {
*/
private void reportSvStatus() {
int svCount = native_read_sv_status(mSvidWithFlags,
- mCn0s,
- mSvElevations,
- mSvAzimuths,
- mSvCarrierFreqs);
+ mCn0s,
+ mSvElevations,
+ mSvAzimuths,
+ mSvCarrierFreqs);
mListenerHelper.onSvStatusChanged(
svCount,
mSvidWithFlags,
@@ -1676,7 +1712,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
if ((mSvidWithFlags[i] & GnssStatus.GNSS_SV_FLAGS_USED_IN_FIX) != 0) {
++usedInFixCount;
if (mCn0s[i] > maxCn0) {
- maxCn0 = (int)mCn0s[i];
+ maxCn0 = (int) mCn0s[i];
}
meanCn0 += mCn0s[i];
}
@@ -1693,7 +1729,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
((mSvidWithFlags[i] & GnssStatus.GNSS_SV_FLAGS_USED_IN_FIX) == 0
? "" : "U") +
((mSvidWithFlags[i] & GnssStatus.GNSS_SV_FLAGS_HAS_CARRIER_FREQUENCY) == 0
- ? "" : "F"));
+ ? "" : "F"));
}
}
if (usedInFixCount > 0) {
@@ -1703,7 +1739,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
updateStatus(mStatus, usedInFixCount, meanCn0, maxCn0);
if (mNavigating && mStatus == LocationProvider.AVAILABLE && mLastFixTime > 0 &&
- SystemClock.elapsedRealtime() - mLastFixTime > RECENT_FIX_TIMEOUT) {
+ SystemClock.elapsedRealtime() - mLastFixTime > RECENT_FIX_TIMEOUT) {
// send an intent to notify that the GPS is no longer receiving fixes.
Intent intent = new Intent(LocationManager.GPS_FIX_CHANGE_ACTION);
intent.putExtra(LocationManager.EXTRA_GPS_ENABLED, false);
@@ -1797,33 +1833,53 @@ public class GnssLocationProvider implements LocationProviderInterface {
/**
* called from native code to inform us what the GPS engine capabilities are
*/
- private void setEngineCapabilities(int capabilities) {
- mEngineCapabilities = capabilities;
+ private void setEngineCapabilities(final int capabilities) {
+ // send to handler thread for fast native return, and in-order handling
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mEngineCapabilities = capabilities;
- if (hasCapability(GPS_CAPABILITY_ON_DEMAND_TIME)) {
- mOnDemandTimeInjection = true;
- requestUtcTime();
- }
+ if (hasCapability(GPS_CAPABILITY_ON_DEMAND_TIME)) {
+ mOnDemandTimeInjection = true;
+ requestUtcTime();
+ }
- mGnssMeasurementsProvider.onCapabilitiesUpdated(
- (capabilities & GPS_CAPABILITY_MEASUREMENTS) == GPS_CAPABILITY_MEASUREMENTS);
- mGnssNavigationMessageProvider.onCapabilitiesUpdated(
- (capabilities & GPS_CAPABILITY_NAV_MESSAGES) == GPS_CAPABILITY_NAV_MESSAGES);
- }
+ mGnssMeasurementsProvider.onCapabilitiesUpdated(hasCapability(
+ GPS_CAPABILITY_MEASUREMENTS));
+ mGnssNavigationMessageProvider.onCapabilitiesUpdated(hasCapability(
+ GPS_CAPABILITY_NAV_MESSAGES));
+ }
+ });
+ }
/**
- * Called from native code to inform us the hardware information.
+ * Called from native code to inform us the hardware year.
*/
- private void setGnssYearOfHardware(int yearOfHardware) {
+ private void setGnssYearOfHardware(final int yearOfHardware) {
+ // mHardwareYear is simply set here, to be read elsewhere, and is volatile for safe sync
if (DEBUG) Log.d(TAG, "setGnssYearOfHardware called with " + yearOfHardware);
- mYearOfHardware = yearOfHardware;
+ mHardwareYear = yearOfHardware;
+ }
+
+ /**
+ * Called from native code to inform us the hardware model name.
+ */
+ private void setGnssHardwareModelName(final String modelName) {
+ // mHardwareModelName is simply set here, to be read elsewhere, and volatile for safe sync
+ if (DEBUG) Log.d(TAG, "setGnssModelName called with " + modelName);
+ mHardwareModelName = modelName;
}
public interface GnssSystemInfoProvider {
/**
- * Returns the year of GPS hardware.
+ * Returns the year of underlying GPS hardware.
*/
int getGnssYearOfHardware();
+ /**
+ * Returns the model name of underlying GPS hardware.
+ */
+ String getGnssHardwareModelName();
}
/**
@@ -1833,7 +1889,11 @@ public class GnssLocationProvider implements LocationProviderInterface {
return new GnssSystemInfoProvider() {
@Override
public int getGnssYearOfHardware() {
- return mYearOfHardware;
+ return mHardwareYear;
+ }
+ @Override
+ public String getGnssHardwareModelName() {
+ return mHardwareModelName;
}
};
}
@@ -1843,14 +1903,17 @@ public class GnssLocationProvider implements LocationProviderInterface {
* Returns the GNSS batching size
*/
int getSize();
+
/**
* Starts the hardware batching operation
*/
boolean start(long periodNanos, boolean wakeOnFifoFull);
+
/**
* Forces a flush of existing locations from the hardware batching
*/
void flush();
+
/**
* Stops the batching operation
*/
@@ -1866,6 +1929,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
public int getSize() {
return native_get_batch_size();
}
+
@Override
public boolean start(long periodNanos, boolean wakeOnFifoFull) {
if (periodNanos <= 0) {
@@ -1875,10 +1939,12 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
return native_start_batch(periodNanos, wakeOnFifoFull);
}
+
@Override
public void flush() {
native_flush_batch();
}
+
@Override
public boolean stop() {
return native_stop_batch();
@@ -1911,7 +1977,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
private void enableBatching() {
if (!native_init_batching()) {
Log.e(TAG, "Failed to initialize GNSS batching");
- };
+ }
}
/**
@@ -1927,7 +1993,9 @@ public class GnssLocationProvider implements LocationProviderInterface {
*/
private void reportLocationBatch(Location[] locationArray) {
List<Location> locations = new ArrayList<>(Arrays.asList(locationArray));
- if(DEBUG) { Log.d(TAG, "Location batch of size " + locationArray.length + "reported"); }
+ if (DEBUG) {
+ Log.d(TAG, "Location batch of size " + locationArray.length + " reported");
+ }
try {
mILocationManager.reportLocationBatch(locations);
} catch (RemoteException e) {
@@ -1947,7 +2015,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
* Converts the GPS HAL status to the internal Geofence Hardware status.
*/
private int getGeofenceStatus(int status) {
- switch(status) {
+ switch (status) {
case GPS_GEOFENCE_OPERATION_SUCCESS:
return GeofenceHardware.GEOFENCE_SUCCESS;
case GPS_GEOFENCE_ERROR_GENERIC:
@@ -1970,7 +2038,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
* All geofence callbacks are called on the same thread
*/
private void reportGeofenceTransition(int geofenceId, Location location, int transition,
- long transitionTimestamp) {
+ long transitionTimestamp) {
if (mGeofenceHardwareImpl == null) {
mGeofenceHardwareImpl = GeofenceHardwareImpl.getInstance(mContext);
}
@@ -1992,7 +2060,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
mGeofenceHardwareImpl = GeofenceHardwareImpl.getInstance(mContext);
}
int monitorStatus = GeofenceHardware.MONITOR_CURRENTLY_UNAVAILABLE;
- if(status == GPS_GEOFENCE_AVAILABLE) {
+ if (status == GPS_GEOFENCE_AVAILABLE) {
monitorStatus = GeofenceHardware.MONITOR_CURRENTLY_AVAILABLE;
}
mGeofenceHardwareImpl.reportGeofenceMonitorStatus(
@@ -2048,12 +2116,13 @@ public class GnssLocationProvider implements LocationProviderInterface {
private final INetInitiatedListener mNetInitiatedListener = new INetInitiatedListener.Stub() {
// Sends a response for an NI request to HAL.
@Override
- public boolean sendNiResponse(int notificationId, int userResponse)
- {
+ public boolean sendNiResponse(int notificationId, int userResponse) {
// TODO Add Permission check
- if (DEBUG) Log.d(TAG, "sendNiResponse, notifId: " + notificationId +
- ", response: " + userResponse);
+ if (DEBUG) {
+ Log.d(TAG, "sendNiResponse, notifId: " + notificationId +
+ ", response: " + userResponse);
+ }
native_send_ni_response(notificationId, userResponse);
return true;
}
@@ -2074,8 +2143,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
String text,
int requestorIdEncoding,
int textEncoding
- )
- {
+ ) {
Log.i(TAG, "reportNiNotification: entered");
Log.i(TAG, "notificationId: " + notificationId +
", niType: " + niType +
@@ -2094,7 +2162,8 @@ public class GnssLocationProvider implements LocationProviderInterface {
notification.niType = niType;
notification.needNotify = (notifyFlags & GpsNetInitiatedHandler.GPS_NI_NEED_NOTIFY) != 0;
notification.needVerify = (notifyFlags & GpsNetInitiatedHandler.GPS_NI_NEED_VERIFY) != 0;
- notification.privacyOverride = (notifyFlags & GpsNetInitiatedHandler.GPS_NI_PRIVACY_OVERRIDE) != 0;
+ notification.privacyOverride =
+ (notifyFlags & GpsNetInitiatedHandler.GPS_NI_PRIVACY_OVERRIDE) != 0;
notification.timeout = timeout;
notification.defaultResponse = defaultResponse;
notification.requestorId = requestorId;
@@ -2126,8 +2195,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
data = data_temp;
type = AGPS_SETID_TYPE_IMSI;
}
- }
- else if ((flags & AGPS_RIL_REQUEST_SETID_MSISDN) == AGPS_RIL_REQUEST_SETID_MSISDN) {
+ } else if ((flags & AGPS_RIL_REQUEST_SETID_MSISDN) == AGPS_RIL_REQUEST_SETID_MSISDN) {
String data_temp = phone.getLine1Number();
if (data_temp == null) {
// This means the framework does not have the SIM card ready.
@@ -2161,14 +2229,14 @@ public class GnssLocationProvider implements LocationProviderInterface {
if ((gsm_cell != null) && (phone.getNetworkOperator() != null)
&& (phone.getNetworkOperator().length() > 3)) {
int type;
- int mcc = Integer.parseInt(phone.getNetworkOperator().substring(0,3));
+ int mcc = Integer.parseInt(phone.getNetworkOperator().substring(0, 3));
int mnc = Integer.parseInt(phone.getNetworkOperator().substring(3));
int networkType = phone.getNetworkType();
if (networkType == TelephonyManager.NETWORK_TYPE_UMTS
- || networkType == TelephonyManager.NETWORK_TYPE_HSDPA
- || networkType == TelephonyManager.NETWORK_TYPE_HSUPA
- || networkType == TelephonyManager.NETWORK_TYPE_HSPA
- || networkType == TelephonyManager.NETWORK_TYPE_HSPAP) {
+ || networkType == TelephonyManager.NETWORK_TYPE_HSDPA
+ || networkType == TelephonyManager.NETWORK_TYPE_HSUPA
+ || networkType == TelephonyManager.NETWORK_TYPE_HSPA
+ || networkType == TelephonyManager.NETWORK_TYPE_HSPAP) {
type = AGPS_REF_LOCATION_TYPE_UMTS_CELLID;
} else {
type = AGPS_REF_LOCATION_TYPE_GSM_CELLID;
@@ -2176,7 +2244,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
native_agps_set_ref_location_cellid(type, mcc, mnc,
gsm_cell.getLac(), gsm_cell.getCid());
} else {
- Log.e(TAG,"Error getting cell location info.");
+ Log.e(TAG, "Error getting cell location info.");
}
} else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
Log.e(TAG, "CDMA not supported.");
@@ -2269,7 +2337,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
* restart.
*/
boolean isInitialized = native_init();
- if(!isInitialized) {
+ if (!isInitialized) {
Log.w(TAG, "Native initialization failed at bootup");
} else {
native_cleanup();
@@ -2280,7 +2348,8 @@ public class GnssLocationProvider implements LocationProviderInterface {
reloadGpsProperties(mContext, mProperties);
// TODO: When this object "finishes" we should unregister by invoking
- // SubscriptionManager.getInstance(mContext).unregister(mOnSubscriptionsChangedListener);
+ // SubscriptionManager.getInstance(mContext).unregister
+ // (mOnSubscriptionsChangedListener);
// This is not strictly necessary because it will be unregistered if the
// notification fails but it is good form.
@@ -2359,12 +2428,18 @@ public class GnssLocationProvider implements LocationProviderInterface {
handleUpdateLocation(location);
}
}
+
@Override
- public void onStatusChanged(String provider, int status, Bundle extras) { }
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+
@Override
- public void onProviderEnabled(String provider) { }
+ public void onProviderEnabled(String provider) {
+ }
+
@Override
- public void onProviderDisabled(String provider) { }
+ public void onProviderDisabled(String provider) {
+ }
}
private String getSelectedApn() {
@@ -2373,7 +2448,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
try {
cursor = mContext.getContentResolver().query(
uri,
- new String[] { "apn" },
+ new String[]{"apn"},
null /* selection */,
null /* selectionArgs */,
Carriers.DEFAULT_SORT_ORDER);
@@ -2404,7 +2479,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
try {
cursor = mContext.getContentResolver().query(
Carriers.CONTENT_URI,
- new String[] { Carriers.PROTOCOL },
+ new String[]{Carriers.PROTOCOL},
selection,
null,
Carriers.DEFAULT_SORT_ORDER);
@@ -2461,7 +2536,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
/**
* @return {@code true} if there is a data network available for outgoing connections,
- * {@code false} otherwise.
+ * {@code false} otherwise.
*/
private boolean isDataNetworkConnected() {
NetworkInfo activeNetworkInfo = mConnMgr.getActiveNetworkInfo();
@@ -2482,7 +2557,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
* @return A string representing the current state stored in {@link #mAGpsDataConnectionState}.
*/
private String agpsDataConnStateAsString() {
- switch(mAGpsDataConnectionState) {
+ switch (mAGpsDataConnectionState) {
case AGPS_DATA_CONNECTION_CLOSED:
return "CLOSED";
case AGPS_DATA_CONNECTION_OPEN:
@@ -2549,12 +2624,12 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
-
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
StringBuilder s = new StringBuilder();
s.append(" mStarted=").append(mStarted).append('\n');
s.append(" mFixInterval=").append(mFixInterval).append('\n');
+ s.append(" mLowPowerMode=").append(mLowPowerMode).append('\n');
s.append(" mDisableGps (battery saver mode)=").append(mDisableGps).append('\n');
s.append(" mEngineCapabilities=0x").append(Integer.toHexString(mEngineCapabilities));
s.append(" ( ");
@@ -2618,29 +2693,45 @@ public class GnssLocationProvider implements LocationProviderInterface {
// preallocated to avoid memory allocation in reportNmea()
private byte[] mNmeaBuffer = new byte[120];
- static { class_init_native(); }
+ static {
+ class_init_native();
+ }
+
private static native void class_init_native();
+
private static native boolean native_is_supported();
+
private static native boolean native_is_agps_ril_supported();
+
private static native boolean native_is_gnss_configuration_supported();
private native boolean native_init();
+
private native void native_cleanup();
+
private native boolean native_set_position_mode(int mode, int recurrence, int min_interval,
- int preferred_accuracy, int preferred_time);
+ int preferred_accuracy, int preferred_time, boolean lowPowerMode);
+
private native boolean native_start();
+
private native boolean native_stop();
+
private native void native_delete_aiding_data(int flags);
+
// returns number of SVs
// mask[0] is ephemeris mask and mask[1] is almanac mask
private native int native_read_sv_status(int[] prnWithFlags, float[] cn0s, float[] elevations,
float[] azimuths, float[] carrierFrequencies);
+
private native int native_read_nmea(byte[] buffer, int bufferSize);
+
private native void native_inject_location(double latitude, double longitude, float accuracy);
// XTRA Support
private native void native_inject_time(long time, long timeReference, int uncertainty);
+
private native boolean native_supports_xtra();
+
private native void native_inject_xtra_data(byte[] data, int length);
// DEBUG Support
@@ -2648,9 +2739,13 @@ public class GnssLocationProvider implements LocationProviderInterface {
// AGPS Support
private native void native_agps_data_conn_open(String apn, int apnIpType);
+
private native void native_agps_data_conn_closed();
+
private native void native_agps_data_conn_failed();
- private native void native_agps_ni_message(byte [] msg, int length);
+
+ private native void native_agps_ni_message(byte[] msg, int length);
+
private native void native_set_agps_server(int type, String hostname, int port);
// Network-initiated (NI) Support
@@ -2659,6 +2754,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
// AGPS ril suport
private native void native_agps_set_ref_location_cellid(int type, int mcc, int mnc,
int lac, int cid);
+
private native void native_agps_set_id(int type, String setid);
private native void native_update_network_state(boolean connected, int type,
@@ -2666,38 +2762,57 @@ public class GnssLocationProvider implements LocationProviderInterface {
// Hardware Geofence support.
private static native boolean native_is_geofence_supported();
+
private static native boolean native_add_geofence(int geofenceId, double latitude,
- double longitude, double radius, int lastTransition,int monitorTransitions,
+ double longitude, double radius, int lastTransition, int monitorTransitions,
int notificationResponsivenes, int unknownTimer);
+
private static native boolean native_remove_geofence(int geofenceId);
+
private static native boolean native_resume_geofence(int geofenceId, int transitions);
+
private static native boolean native_pause_geofence(int geofenceId);
// Gps Hal measurements support.
private static native boolean native_is_measurement_supported();
- private native boolean native_start_measurement_collection();
+
+ private native boolean native_start_measurement_collection(boolean enableFullTracking);
+
private native boolean native_stop_measurement_collection();
// Gps Navigation message support.
private static native boolean native_is_navigation_message_supported();
+
private native boolean native_start_navigation_message_collection();
+
private native boolean native_stop_navigation_message_collection();
// GNSS Configuration
private static native boolean native_set_supl_version(int version);
+
private static native boolean native_set_supl_mode(int mode);
+
private static native boolean native_set_supl_es(int es);
+
private static native boolean native_set_lpp_profile(int lppProfile);
+
private static native boolean native_set_gnss_pos_protocol_select(int gnssPosProtocolSelect);
+
private static native boolean native_set_gps_lock(int gpsLock);
+
private static native boolean native_set_emergency_supl_pdn(int emergencySuplPdn);
// GNSS Batching
private static native int native_get_batch_size();
+
private static native boolean native_start_batch(long periodNanos, boolean wakeOnFifoFull);
+
private static native void native_flush_batch();
+
private static native boolean native_stop_batch();
+
private static native boolean native_init_batching();
+
private static native void native_cleanup_batching();
}
diff --git a/com/android/server/location/GnssMeasurementsProvider.java b/com/android/server/location/GnssMeasurementsProvider.java
index 924520b5..477dae65 100644
--- a/com/android/server/location/GnssMeasurementsProvider.java
+++ b/com/android/server/location/GnssMeasurementsProvider.java
@@ -40,11 +40,11 @@ public abstract class GnssMeasurementsProvider
public void onMeasurementsAvailable(final GnssMeasurementsEvent event) {
ListenerOperation<IGnssMeasurementsListener> operation =
new ListenerOperation<IGnssMeasurementsListener>() {
- @Override
- public void execute(IGnssMeasurementsListener listener) throws RemoteException {
- listener.onGnssMeasurementsReceived(event);
- }
- };
+ @Override
+ public void execute(IGnssMeasurementsListener listener) throws RemoteException {
+ listener.onGnssMeasurementsReceived(event);
+ }
+ };
foreach(operation);
}
@@ -70,6 +70,9 @@ public abstract class GnssMeasurementsProvider
case RESULT_INTERNAL_ERROR:
status = GnssMeasurementsEvent.Callback.STATUS_NOT_SUPPORTED;
break;
+ case RESULT_NOT_ALLOWED:
+ status = GnssMeasurementsEvent.Callback.STATUS_NOT_ALLOWED;
+ break;
case RESULT_GPS_LOCATION_DISABLED:
status = GnssMeasurementsEvent.Callback.STATUS_LOCATION_DISABLED;
break;
diff --git a/com/android/server/location/GnssStatusListenerHelper.java b/com/android/server/location/GnssStatusListenerHelper.java
index fe2bb38d..124220f1 100644
--- a/com/android/server/location/GnssStatusListenerHelper.java
+++ b/com/android/server/location/GnssStatusListenerHelper.java
@@ -30,8 +30,8 @@ abstract class GnssStatusListenerHelper extends RemoteListenerHelper<IGnssStatus
}
@Override
- protected boolean registerWithService() {
- return true;
+ protected int registerWithService() {
+ return RemoteListenerHelper.RESULT_SUCCESS;
}
@Override
diff --git a/com/android/server/location/NanoAppStateManager.java b/com/android/server/location/NanoAppStateManager.java
new file mode 100644
index 00000000..98696265
--- /dev/null
+++ b/com/android/server/location/NanoAppStateManager.java
@@ -0,0 +1,192 @@
+/*
+ * 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 com.android.server.location;
+
+import android.annotation.Nullable;
+import android.hardware.contexthub.V1_0.HubAppInfo;
+import android.hardware.location.NanoAppInstanceInfo;
+import android.util.Log;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Manages the state of loaded nanoapps at the Context Hubs.
+ *
+ * This class maintains a list of nanoapps that have been informed as loaded at the hubs. The state
+ * should be updated based on the hub callbacks (defined in IContexthubCallback.hal), as a result
+ * of either loadNanoApp, unloadNanoApp, or queryApps.
+ *
+ * The state tracked by this manager is used by clients of ContextHubService that use the old APIs.
+ *
+ * TODO(b/69270990): Remove this class and its logic once the old API is deprecated.
+ *
+ * @hide
+ */
+/* package */ class NanoAppStateManager {
+ private static final String TAG = "NanoAppStateManager";
+
+ /*
+ * Enables verbose debug logs for this class.
+ */
+ private static final boolean ENABLE_LOG_DEBUG = true;
+
+ /*
+ * Service cache maintaining of handle to nanoapp infos.
+ */
+ private final HashMap<Integer, NanoAppInstanceInfo> mNanoAppHash = new HashMap<>();
+
+ /*
+ * The next nanoapp handle to use.
+ */
+ private int mNextHandle = 0;
+
+ /**
+ * @param nanoAppHandle the nanoapp handle
+ * @return the NanoAppInstanceInfo for the given nanoapp, or null if the nanoapp does not exist
+ * in the cache
+ */
+ @Nullable
+ /* package */
+ synchronized NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) {
+ return mNanoAppHash.get(nanoAppHandle);
+ }
+
+ /**
+ * @return a collection of NanoAppInstanceInfo objects in the cache
+ */
+ /* package */
+ synchronized Collection<NanoAppInstanceInfo> getNanoAppInstanceInfoCollection() {
+ return mNanoAppHash.values();
+ }
+
+ /**
+ * @param contextHubId the ID of the hub to search for the instance
+ * @param nanoAppId the unique 64-bit ID of the nanoapp
+ * @return the nanoapp handle, -1 if the nanoapp is not in the cache
+ */
+ /* package */
+ synchronized int getNanoAppHandle(int contextHubId, long nanoAppId) {
+ for (NanoAppInstanceInfo info : mNanoAppHash.values()) {
+ if (info.getContexthubId() == contextHubId && info.getAppId() == nanoAppId) {
+ return info.getHandle();
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Adds a nanoapp instance to the cache.
+ *
+ * If the cache already contained the nanoapp, the entry is removed and a new nanoapp handle is
+ * generated.
+ *
+ * @param contextHubId the ID of the hub the nanoapp is loaded in
+ * @param nanoAppId the unique 64-bit ID of the nanoapp
+ * @param nanoAppVersion the version of the nanoapp
+ */
+ /* package */
+ synchronized void addNanoAppInstance(int contextHubId, long nanoAppId, int nanoAppVersion) {
+ removeNanoAppInstance(contextHubId, nanoAppId);
+ if (mNanoAppHash.size() == Integer.MAX_VALUE) {
+ Log.e(TAG, "Error adding nanoapp instance: max limit exceeded");
+ return;
+ }
+
+ int nanoAppHandle = mNextHandle;
+ for (int i = 0; i <= Integer.MAX_VALUE; i++) {
+ if (!mNanoAppHash.containsKey(nanoAppHandle)) {
+ mNanoAppHash.put(nanoAppHandle, new NanoAppInstanceInfo(
+ nanoAppHandle, nanoAppId, nanoAppVersion, contextHubId));
+ mNextHandle = (nanoAppHandle == Integer.MAX_VALUE) ? 0 : nanoAppHandle + 1;
+ break;
+ }
+ nanoAppHandle = (nanoAppHandle == Integer.MAX_VALUE) ? 0 : nanoAppHandle + 1;
+ }
+
+ if (ENABLE_LOG_DEBUG) {
+ Log.v(TAG, "Added app instance with handle " + nanoAppHandle + " to hub "
+ + contextHubId + ": ID=0x" + Long.toHexString(nanoAppId)
+ + ", version=0x" + Integer.toHexString(nanoAppVersion));
+ }
+ }
+
+ /**
+ * Removes a nanoapp instance from the cache.
+ *
+ * @param nanoAppId the ID of the nanoapp to remove the instance of
+ */
+ /* package */
+ synchronized void removeNanoAppInstance(int contextHubId, long nanoAppId) {
+ int nanoAppHandle = getNanoAppHandle(contextHubId, nanoAppId);
+ mNanoAppHash.remove(nanoAppHandle);
+ }
+
+ /**
+ * Performs a batch update of the nanoapp cache given a nanoapp query response.
+ *
+ * @param contextHubId the ID of the hub the response came from
+ * @param nanoAppInfoList the list of loaded nanoapps
+ */
+ /* package */
+ synchronized void updateCache(int contextHubId, List<HubAppInfo> nanoAppInfoList) {
+ HashSet<Long> nanoAppIdSet = new HashSet<>();
+ for (HubAppInfo appInfo : nanoAppInfoList) {
+ handleQueryAppEntry(contextHubId, appInfo.appId, appInfo.version);
+ nanoAppIdSet.add(appInfo.appId);
+ }
+
+ Iterator<NanoAppInstanceInfo> iterator = mNanoAppHash.values().iterator();
+ while (iterator.hasNext()) {
+ NanoAppInstanceInfo info = iterator.next();
+ if (info.getContexthubId() == contextHubId &&
+ !nanoAppIdSet.contains(info.getAppId())) {
+ iterator.remove();
+ }
+ }
+ }
+
+ /**
+ * If the nanoapp exists in the cache, then the entry is updated. Otherwise, inserts a new
+ * instance of the nanoapp in the cache. This method should only be invoked from updateCache.
+ *
+ * @param contextHubId the ID of the hub the nanoapp is loaded in
+ * @param nanoAppId the unique 64-bit ID of the nanoapp
+ * @param nanoAppVersion the version of the nanoapp
+ */
+ private void handleQueryAppEntry(int contextHubId, long nanoAppId, int nanoAppVersion) {
+ int nanoAppHandle = getNanoAppHandle(contextHubId, nanoAppId);
+ if (nanoAppHandle == -1) {
+ addNanoAppInstance(contextHubId, nanoAppId, nanoAppVersion);
+ } else {
+ NanoAppInstanceInfo info = mNanoAppHash.get(nanoAppHandle);
+ if (info.getAppVersion() != nanoAppVersion) {
+ mNanoAppHash.put(nanoAppHandle, new NanoAppInstanceInfo(
+ nanoAppHandle, nanoAppId, nanoAppVersion, contextHubId));
+ if (ENABLE_LOG_DEBUG) {
+ Log.v(TAG, "Updated app instance with handle " + nanoAppHandle + " at hub "
+ + contextHubId + ": ID=0x" + Long.toHexString(nanoAppId)
+ + ", version=0x" + Integer.toHexString(nanoAppVersion));
+ }
+ }
+ }
+ }
+}
diff --git a/com/android/server/location/RemoteListenerHelper.java b/com/android/server/location/RemoteListenerHelper.java
index f51bc871..58a95162 100644
--- a/com/android/server/location/RemoteListenerHelper.java
+++ b/com/android/server/location/RemoteListenerHelper.java
@@ -16,8 +16,6 @@
package com.android.server.location;
-import com.android.internal.util.Preconditions;
-
import android.annotation.NonNull;
import android.os.Handler;
import android.os.IBinder;
@@ -25,7 +23,8 @@ import android.os.IInterface;
import android.os.RemoteException;
import android.util.Log;
-import java.lang.Runnable;
+import com.android.internal.util.Preconditions;
+
import java.util.HashMap;
import java.util.Map;
@@ -40,6 +39,7 @@ abstract class RemoteListenerHelper<TListener extends IInterface> {
protected static final int RESULT_GPS_LOCATION_DISABLED = 3;
protected static final int RESULT_INTERNAL_ERROR = 4;
protected static final int RESULT_UNKNOWN = 5;
+ protected static final int RESULT_NOT_ALLOWED = 6;
private final Handler mHandler;
private final String mTag;
@@ -118,7 +118,8 @@ abstract class RemoteListenerHelper<TListener extends IInterface> {
protected abstract boolean isAvailableInPlatform();
protected abstract boolean isGpsEnabled();
- protected abstract boolean registerWithService(); // must access only on handler thread
+ // must access only on handler thread
+ protected abstract int registerWithService();
protected abstract void unregisterFromService(); // must access only on handler thread
protected abstract ListenerOperation<TListener> getHandlerOperation(int result);
@@ -177,10 +178,12 @@ abstract class RemoteListenerHelper<TListener extends IInterface> {
private void tryRegister() {
mHandler.post(new Runnable() {
+ int registrationState = RESULT_INTERNAL_ERROR;
@Override
public void run() {
if (!mIsRegistered) {
- mIsRegistered = registerWithService();
+ registrationState = registerWithService();
+ mIsRegistered = registrationState == RESULT_SUCCESS;
}
if (!mIsRegistered) {
// post back a failure
@@ -188,7 +191,7 @@ abstract class RemoteListenerHelper<TListener extends IInterface> {
@Override
public void run() {
synchronized (mListenerMap) {
- ListenerOperation<TListener> operation = getHandlerOperation(RESULT_INTERNAL_ERROR);
+ ListenerOperation<TListener> operation = getHandlerOperation(registrationState);
foreachUnsafe(operation);
}
}
diff --git a/com/android/server/locksettings/LockSettingsService.java b/com/android/server/locksettings/LockSettingsService.java
index 60f451ab..482acefa 100644
--- a/com/android/server/locksettings/LockSettingsService.java
+++ b/com/android/server/locksettings/LockSettingsService.java
@@ -28,6 +28,8 @@ import static com.android.internal.widget.LockPatternUtils.USER_FRP;
import static com.android.internal.widget.LockPatternUtils.frpCredentialEnabled;
import static com.android.internal.widget.LockPatternUtils.userOwnsFrpCredential;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.IActivityManager;
@@ -36,6 +38,7 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
+import android.app.admin.DevicePolicyManagerInternal;
import android.app.admin.PasswordMetrics;
import android.app.backup.BackupManager;
import android.app.trust.IStrongAuthTracker;
@@ -60,6 +63,7 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
import android.os.ShellCallback;
import android.os.StrictMode;
import android.os.SystemProperties;
@@ -75,6 +79,10 @@ import android.security.keystore.AndroidKeyStoreProvider;
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
import android.security.keystore.UserNotAuthenticatedException;
+import android.security.recoverablekeystore.KeyEntryRecoveryData;
+import android.security.recoverablekeystore.KeyStoreRecoveryData;
+import android.security.recoverablekeystore.KeyStoreRecoveryMetadata;
+import android.security.recoverablekeystore.RecoverableKeyStoreLoader.RecoverableKeyStoreLoaderException;
import android.service.gatekeeper.GateKeeperResponse;
import android.service.gatekeeper.IGateKeeperService;
import android.text.TextUtils;
@@ -83,6 +91,7 @@ import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
@@ -93,11 +102,13 @@ import com.android.internal.widget.ICheckCredentialProgressCallback;
import com.android.internal.widget.ILockSettings;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.VerifyCredentialResponse;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.locksettings.LockSettingsStorage.CredentialHash;
+import com.android.server.locksettings.LockSettingsStorage.PersistentData;
+import com.android.server.locksettings.recoverablekeystore.RecoverableKeyStoreManager;
import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationResult;
import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationToken;
-import com.android.server.locksettings.LockSettingsStorage.PersistentData;
import libcore.util.HexEncoding;
@@ -169,6 +180,8 @@ public class LockSettingsService extends ILockSettings.Stub {
private final KeyStore mKeyStore;
+ private final RecoverableKeyStoreManager mRecoverableKeyStoreManager;
+
private boolean mFirstCallToVold;
protected IGateKeeperService mGateKeeperService;
@@ -367,6 +380,10 @@ public class LockSettingsService extends ILockSettings.Stub {
return KeyStore.getInstance();
}
+ public RecoverableKeyStoreManager getRecoverableKeyStoreManager() {
+ return RecoverableKeyStoreManager.getInstance(mContext);
+ }
+
public IStorageManager getStorageManager() {
final IBinder service = ServiceManager.getService("mount");
if (service != null) {
@@ -393,6 +410,7 @@ public class LockSettingsService extends ILockSettings.Stub {
mInjector = injector;
mContext = injector.getContext();
mKeyStore = injector.getKeyStore();
+ mRecoverableKeyStoreManager = injector.getRecoverableKeyStoreManager();
mHandler = injector.getHandler();
mStrongAuth = injector.getStrongAuth();
mActivityManager = injector.getActivityManager();
@@ -876,16 +894,28 @@ public class LockSettingsService extends ILockSettings.Stub {
String managedUserPassword) {
checkWritePermission(userId);
synchronized (mSeparateChallengeLock) {
- setBoolean(SEPARATE_PROFILE_CHALLENGE_KEY, enabled, userId);
- if (enabled) {
- mStorage.removeChildProfileLock(userId);
- removeKeystoreProfileKey(userId);
- } else {
- tieManagedProfileLockIfNecessary(userId, managedUserPassword);
- }
+ setSeparateProfileChallengeEnabledLocked(userId, enabled, managedUserPassword);
+ }
+ notifySeparateProfileChallengeChanged(userId);
+ }
+
+ @GuardedBy("mSeparateChallengeLock")
+ private void setSeparateProfileChallengeEnabledLocked(@UserIdInt int userId, boolean enabled,
+ String managedUserPassword) {
+ setBoolean(SEPARATE_PROFILE_CHALLENGE_KEY, enabled, userId);
+ if (enabled) {
+ mStorage.removeChildProfileLock(userId);
+ removeKeystoreProfileKey(userId);
+ } else {
+ tieManagedProfileLockIfNecessary(userId, managedUserPassword);
}
}
+ private void notifySeparateProfileChallengeChanged(int userId) {
+ LocalServices.getService(DevicePolicyManagerInternal.class)
+ .reportSeparateProfileChallengeChanged(userId);
+ }
+
@Override
public void setBoolean(String key, boolean value, int userId) {
checkWritePermission(userId);
@@ -1219,9 +1249,10 @@ public class LockSettingsService extends ILockSettings.Stub {
checkWritePermission(userId);
synchronized (mSeparateChallengeLock) {
setLockCredentialInternal(credential, type, savedCredential, requestedQuality, userId);
- setSeparateProfileChallengeEnabled(userId, true, null);
+ setSeparateProfileChallengeEnabledLocked(userId, true, null);
notifyPasswordChanged(userId);
}
+ notifySeparateProfileChallengeChanged(userId);
}
private void setLockCredentialInternal(String credential, int credentialType,
@@ -1550,6 +1581,8 @@ public class LockSettingsService extends ILockSettings.Stub {
userId, progressCallback);
// The user employs synthetic password based credential.
if (response != null) {
+ mRecoverableKeyStoreManager.lockScreenSecretAvailable(credentialType, credential,
+ userId);
return response;
}
@@ -1674,6 +1707,9 @@ public class LockSettingsService extends ILockSettings.Stub {
/* TODO(roosa): keep the same password quality */, userId);
if (!hasChallenge) {
notifyActivePasswordMetricsAvailable(credential, userId);
+ // Use credentials to create recoverable keystore snapshot.
+ mRecoverableKeyStoreManager.lockScreenSecretAvailable(
+ storedHash.type, credential, userId);
return VerifyCredentialResponse.OK;
}
// Fall through to get the auth token. Technically this should never happen,
@@ -1726,6 +1762,10 @@ public class LockSettingsService extends ILockSettings.Stub {
}
}
}
+ // Use credentials to create recoverable keystore snapshot.
+ mRecoverableKeyStoreManager.lockScreenSecretAvailable(storedHash.type, credential,
+ userId);
+
} else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
if (response.getTimeout() > 0) {
requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId);
@@ -1914,6 +1954,90 @@ public class LockSettingsService extends ILockSettings.Stub {
}
}
+ @Override
+ public void initRecoveryService(@NonNull String rootCertificateAlias,
+ @NonNull byte[] signedPublicKeyList, @UserIdInt int userId)
+ throws RemoteException {
+ mRecoverableKeyStoreManager.initRecoveryService(rootCertificateAlias,
+ signedPublicKeyList, userId);
+ }
+
+ @Override
+ public KeyStoreRecoveryData getRecoveryData(@NonNull byte[] account, @UserIdInt int userId)
+ throws RemoteException {
+ return mRecoverableKeyStoreManager.getRecoveryData(account, userId);
+ }
+
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent, int userId)
+ throws RemoteException {
+ mRecoverableKeyStoreManager.setSnapshotCreatedPendingIntent(intent, userId);
+ }
+
+ public Map getRecoverySnapshotVersions(int userId) throws RemoteException {
+ return mRecoverableKeyStoreManager.getRecoverySnapshotVersions(userId);
+ }
+
+ @Override
+ public void setServerParameters(long serverParameters, @UserIdInt int userId)
+ throws RemoteException {
+ mRecoverableKeyStoreManager.setServerParameters(serverParameters, userId);
+ }
+
+ @Override
+ public void setRecoveryStatus(@NonNull String packageName, @Nullable String[] aliases,
+ int status, @UserIdInt int userId) throws RemoteException {
+ mRecoverableKeyStoreManager.setRecoveryStatus(packageName, aliases, status, userId);
+ }
+
+ public Map getRecoveryStatus(@Nullable String packageName, int userId) throws RemoteException {
+ return mRecoverableKeyStoreManager.getRecoveryStatus(packageName, userId);
+ }
+
+ @Override
+ public void setRecoverySecretTypes(@NonNull @KeyStoreRecoveryMetadata.UserSecretType
+ int[] secretTypes, @UserIdInt int userId) throws RemoteException {
+ mRecoverableKeyStoreManager.setRecoverySecretTypes(secretTypes, userId);
+ }
+
+ @Override
+ public int[] getRecoverySecretTypes(@UserIdInt int userId) throws RemoteException {
+ return mRecoverableKeyStoreManager.getRecoverySecretTypes(userId);
+
+ }
+
+ @Override
+ public int[] getPendingRecoverySecretTypes(@UserIdInt int userId) throws RemoteException {
+ throw new SecurityException("Not implemented");
+ }
+
+ @Override
+ public void recoverySecretAvailable(@NonNull KeyStoreRecoveryMetadata recoverySecret,
+ @UserIdInt int userId) throws RemoteException {
+ mRecoverableKeyStoreManager.recoverySecretAvailable(recoverySecret, userId);
+ }
+
+ @Override
+ public byte[] startRecoverySession(@NonNull String sessionId,
+ @NonNull byte[] verifierPublicKey, @NonNull byte[] vaultParams,
+ @NonNull byte[] vaultChallenge, @NonNull List<KeyStoreRecoveryMetadata> secrets,
+ @UserIdInt int userId) throws RemoteException {
+ return mRecoverableKeyStoreManager.startRecoverySession(sessionId, verifierPublicKey,
+ vaultParams, vaultChallenge, secrets, userId);
+ }
+
+ @Override
+ public Map<String, byte[]> recoverKeys(@NonNull String sessionId, @NonNull byte[] recoveryKeyBlob,
+ @NonNull List<KeyEntryRecoveryData> applicationKeys, @UserIdInt int userId)
+ throws RemoteException {
+ return mRecoverableKeyStoreManager.recoverKeys(
+ sessionId, recoveryKeyBlob, applicationKeys, userId);
+ }
+
+ @Override
+ public byte[] generateAndStoreKey(@NonNull String alias) throws RemoteException {
+ return mRecoverableKeyStoreManager.generateAndStoreKey(alias);
+ }
+
private static final String[] VALID_SETTINGS = new String[] {
LockPatternUtils.LOCKOUT_PERMANENT_KEY,
LockPatternUtils.LOCKOUT_ATTEMPT_DEADLINE,
@@ -2344,9 +2468,10 @@ public class LockSettingsService extends ILockSettings.Stub {
}
if (result) {
synchronized (mSeparateChallengeLock) {
- setSeparateProfileChallengeEnabled(userId, true, null);
+ setSeparateProfileChallengeEnabledLocked(userId, true, null);
}
notifyPasswordChanged(userId);
+ notifySeparateProfileChallengeChanged(userId);
}
return result;
}
diff --git a/com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java b/com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java
new file mode 100644
index 00000000..9a4d0511
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.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 com.android.server.locksettings.recoverablekeystore;
+
+import android.security.keystore.AndroidKeyStoreProvider;
+
+import java.security.KeyStoreException;
+import java.security.NoSuchProviderException;
+
+public interface AndroidKeyStoreFactory {
+ KeyStoreProxy getKeyStoreForUid(int uid) throws KeyStoreException, NoSuchProviderException;
+
+ class Impl implements AndroidKeyStoreFactory {
+ @Override
+ public KeyStoreProxy getKeyStoreForUid(int uid)
+ throws KeyStoreException, NoSuchProviderException {
+ return new KeyStoreProxyImpl(AndroidKeyStoreProvider.getKeyStoreForUid(uid));
+ }
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/BadPlatformKeyException.java b/com/android/server/locksettings/recoverablekeystore/BadPlatformKeyException.java
new file mode 100644
index 00000000..c7a98b66
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/BadPlatformKeyException.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+/**
+ * Error thrown when the {@link PlatformDecryptionKey} instance has a different generation ID from
+ * the {@link WrappedKey} instance.
+ *
+ * @hide
+ */
+public class BadPlatformKeyException extends Exception {
+
+ /**
+ * A new instance with {@code message}.
+ *
+ * @hide
+ */
+ public BadPlatformKeyException(String message) {
+ super(message);
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java b/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java
new file mode 100644
index 00000000..5155a99e
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+/**
+ * Error thrown initializing {@link PlatformKeyManager} if the user is not secure (i.e., has no
+ * lock screen set).
+ */
+public class InsecureUserException extends Exception {
+
+ /**
+ * A new instance with {@code message} error message.
+ */
+ public InsecureUserException(String message) {
+ super(message);
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java b/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java
new file mode 100644
index 00000000..81031770
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.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 com.android.server.locksettings.recoverablekeystore;
+
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+
+/**
+ * Proxies {@link java.security.KeyStore}. As all of its methods are final, it cannot otherwise be
+ * mocked for tests.
+ *
+ * @hide
+ */
+public interface KeyStoreProxy {
+
+ /** @see KeyStore#containsAlias(String) */
+ boolean containsAlias(String alias) throws KeyStoreException;
+
+ /** @see KeyStore#getKey(String, char[]) */
+ Key getKey(String alias, char[] password)
+ throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException;
+
+ /** @see KeyStore#setEntry(String, KeyStore.Entry, KeyStore.ProtectionParameter) */
+ void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam)
+ throws KeyStoreException;
+
+ /** @see KeyStore#deleteEntry(String) */
+ void deleteEntry(String alias) throws KeyStoreException;
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java b/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java
new file mode 100644
index 00000000..59132da7
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.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 com.android.server.locksettings.recoverablekeystore;
+
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+
+/**
+ * Implementation of {@link KeyStoreProxy} that delegates all method calls to the {@link KeyStore}.
+ */
+public class KeyStoreProxyImpl implements KeyStoreProxy {
+
+ private final KeyStore mKeyStore;
+
+ /**
+ * A new instance, delegating to {@code keyStore}.
+ */
+ public KeyStoreProxyImpl(KeyStore keyStore) {
+ mKeyStore = keyStore;
+ }
+
+ @Override
+ public boolean containsAlias(String alias) throws KeyStoreException {
+ return mKeyStore.containsAlias(alias);
+ }
+
+ @Override
+ public Key getKey(String alias, char[] password)
+ throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+ return mKeyStore.getKey(alias, password);
+ }
+
+ @Override
+ public void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam)
+ throws KeyStoreException {
+ mKeyStore.setEntry(alias, entry, protParam);
+ }
+
+ @Override
+ public void deleteEntry(String alias) throws KeyStoreException {
+ mKeyStore.deleteEntry(alias);
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
new file mode 100644
index 00000000..e385833b
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
@@ -0,0 +1,350 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_LOCKSCREEN;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.security.recoverablekeystore.KeyDerivationParameters;
+import android.security.recoverablekeystore.KeyEntryRecoveryData;
+import android.security.recoverablekeystore.KeyStoreRecoveryData;
+import android.security.recoverablekeystore.KeyStoreRecoveryMetadata;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.KeyStoreException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+/**
+ * Task to sync application keys to a remote vault service.
+ *
+ * @hide
+ */
+public class KeySyncTask implements Runnable {
+ private static final String TAG = "KeySyncTask";
+
+ private static final String RECOVERY_KEY_ALGORITHM = "AES";
+ private static final int RECOVERY_KEY_SIZE_BITS = 256;
+ private static final int SALT_LENGTH_BYTES = 16;
+ private static final int LENGTH_PREFIX_BYTES = Integer.BYTES;
+ private static final String LOCK_SCREEN_HASH_ALGORITHM = "SHA-256";
+ private static final int TRUSTED_HARDWARE_MAX_ATTEMPTS = 10;
+
+ private final RecoverableKeyStoreDb mRecoverableKeyStoreDb;
+ private final int mUserId;
+ private final int mCredentialType;
+ private final String mCredential;
+ private final PlatformKeyManager.Factory mPlatformKeyManagerFactory;
+ private final RecoverySnapshotStorage mRecoverySnapshotStorage;
+ private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;
+
+ public static KeySyncTask newInstance(
+ Context context,
+ RecoverableKeyStoreDb recoverableKeyStoreDb,
+ RecoverySnapshotStorage snapshotStorage,
+ RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
+ int userId,
+ int credentialType,
+ String credential
+ ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException {
+ return new KeySyncTask(
+ recoverableKeyStoreDb,
+ snapshotStorage,
+ recoverySnapshotListenersStorage,
+ userId,
+ credentialType,
+ credential,
+ () -> PlatformKeyManager.getInstance(context, recoverableKeyStoreDb, userId));
+ }
+
+ /**
+ * A new task.
+ *
+ * @param recoverableKeyStoreDb Database where the keys are stored.
+ * @param userId The uid of the user whose profile has been unlocked.
+ * @param credentialType The type of credential - i.e., pattern or password.
+ * @param credential The credential, encoded as a {@link String}.
+ * @param platformKeyManagerFactory Instantiates a {@link PlatformKeyManager} for the user.
+ * This is a factory to enable unit testing, as otherwise it would be impossible to test
+ * without a screen unlock occurring!
+ */
+ @VisibleForTesting
+ KeySyncTask(
+ RecoverableKeyStoreDb recoverableKeyStoreDb,
+ RecoverySnapshotStorage snapshotStorage,
+ RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
+ int userId,
+ int credentialType,
+ String credential,
+ PlatformKeyManager.Factory platformKeyManagerFactory) {
+ mSnapshotListenersStorage = recoverySnapshotListenersStorage;
+ mRecoverableKeyStoreDb = recoverableKeyStoreDb;
+ mUserId = userId;
+ mCredentialType = credentialType;
+ mCredential = credential;
+ mPlatformKeyManagerFactory = platformKeyManagerFactory;
+ mRecoverySnapshotStorage = snapshotStorage;
+ }
+
+ @Override
+ public void run() {
+ try {
+ syncKeys();
+ } catch (Exception e) {
+ Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e);
+ }
+ }
+
+ private void syncKeys() {
+ if (!isSyncPending()) {
+ Log.d(TAG, "Key sync not needed.");
+ return;
+ }
+
+ int recoveryAgentUid = mRecoverableKeyStoreDb.getRecoveryAgentUid(mUserId);
+ if (recoveryAgentUid == -1) {
+ Log.w(TAG, "No recovery agent initialized for user " + mUserId);
+ return;
+ }
+ if (!mSnapshotListenersStorage.hasListener(recoveryAgentUid)) {
+ Log.w(TAG, "No pending intent registered for recovery agent " + recoveryAgentUid);
+ return;
+ }
+
+ PublicKey publicKey = getVaultPublicKey();
+ if (publicKey == null) {
+ Log.w(TAG, "Not initialized for KeySync: no public key set. Cancelling task.");
+ return;
+ }
+
+ Long deviceId = mRecoverableKeyStoreDb.getServerParameters(mUserId, recoveryAgentUid);
+ if (deviceId == null) {
+ Log.w(TAG, "No device ID set for user " + mUserId);
+ return;
+ }
+
+ byte[] salt = generateSalt();
+ byte[] localLskfHash = hashCredentials(salt, mCredential);
+
+ Map<String, SecretKey> rawKeys;
+ try {
+ rawKeys = getKeysToSync();
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Failed to load recoverable keys for sync", e);
+ return;
+ } catch (InsecureUserException e) {
+ Log.wtf(TAG, "A screen unlock triggered the key sync flow, so user must have "
+ + "lock screen. This should be impossible.", e);
+ return;
+ } catch (BadPlatformKeyException e) {
+ Log.wtf(TAG, "Loaded keys for same generation ID as platform key, so "
+ + "BadPlatformKeyException should be impossible.", e);
+ return;
+ }
+
+ SecretKey recoveryKey;
+ try {
+ recoveryKey = generateRecoveryKey();
+ } catch (NoSuchAlgorithmException e) {
+ Log.wtf("AES should never be unavailable", e);
+ return;
+ }
+
+ Map<String, byte[]> encryptedApplicationKeys;
+ try {
+ encryptedApplicationKeys = KeySyncUtils.encryptKeysWithRecoveryKey(
+ recoveryKey, rawKeys);
+ } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+ Log.wtf(TAG,
+ "Should be impossible: could not encrypt application keys with random key",
+ e);
+ return;
+ }
+
+ // TODO: where do we get counter_id from here?
+ byte[] vaultParams = KeySyncUtils.packVaultParams(
+ publicKey,
+ /*counterId=*/ 1,
+ TRUSTED_HARDWARE_MAX_ATTEMPTS,
+ deviceId);
+
+ byte[] encryptedRecoveryKey;
+ try {
+ encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey(
+ publicKey,
+ localLskfHash,
+ vaultParams,
+ recoveryKey);
+ } catch (NoSuchAlgorithmException e) {
+ Log.wtf(TAG, "SecureBox encrypt algorithms unavailable", e);
+ return;
+ } catch (InvalidKeyException e) {
+ Log.e(TAG,"Could not encrypt with recovery key", e);
+ return;
+ }
+
+ KeyStoreRecoveryMetadata metadata = new KeyStoreRecoveryMetadata(
+ /*userSecretType=*/ TYPE_LOCKSCREEN,
+ /*lockScreenUiFormat=*/ mCredentialType,
+ /*keyDerivationParameters=*/ KeyDerivationParameters.createSHA256Parameters(salt),
+ /*secret=*/ new byte[0]);
+ ArrayList<KeyStoreRecoveryMetadata> metadataList = new ArrayList<>();
+ metadataList.add(metadata);
+
+ // TODO: implement snapshot version
+ mRecoverySnapshotStorage.put(mUserId, new KeyStoreRecoveryData(
+ /*snapshotVersion=*/ 1,
+ /*recoveryMetadata=*/ metadataList,
+ /*applicationKeyBlobs=*/ createApplicationKeyEntries(encryptedApplicationKeys),
+ /*encryptedRecoveryKeyblob=*/ encryptedRecoveryKey));
+ mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid);
+ }
+
+ private PublicKey getVaultPublicKey() {
+ return mRecoverableKeyStoreDb.getRecoveryServicePublicKey(mUserId);
+ }
+
+ /**
+ * Returns all of the recoverable keys for the user.
+ */
+ private Map<String, SecretKey> getKeysToSync()
+ throws InsecureUserException, KeyStoreException, UnrecoverableKeyException,
+ NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException {
+ PlatformKeyManager platformKeyManager = mPlatformKeyManagerFactory.newInstance();
+ PlatformDecryptionKey decryptKey = platformKeyManager.getDecryptKey();
+ Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys(
+ mUserId, decryptKey.getGenerationId());
+ return WrappedKey.unwrapKeys(decryptKey, wrappedKeys);
+ }
+
+ /**
+ * Returns {@code true} if a sync is pending.
+ */
+ private boolean isSyncPending() {
+ // TODO: implement properly. For now just always syncing if the user has any recoverable
+ // keys. We need to keep track of when the store's state actually changes.
+ return !mRecoverableKeyStoreDb.getAllKeys(
+ mUserId, mRecoverableKeyStoreDb.getPlatformKeyGenerationId(mUserId)).isEmpty();
+ }
+
+ /**
+ * The UI best suited to entering the given lock screen. This is synced with the vault so the
+ * user can be shown the same UI when recovering the vault on another device.
+ *
+ * @return The format - either pattern, pin, or password.
+ */
+ @VisibleForTesting
+ @KeyStoreRecoveryMetadata.LockScreenUiFormat static int getUiFormat(
+ int credentialType, String credential) {
+ if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
+ return KeyStoreRecoveryMetadata.TYPE_PATTERN;
+ } else if (isPin(credential)) {
+ return KeyStoreRecoveryMetadata.TYPE_PIN;
+ } else {
+ return KeyStoreRecoveryMetadata.TYPE_PASSWORD;
+ }
+ }
+
+ /**
+ * Generates a salt to include with the lock screen hash.
+ *
+ * @return The salt.
+ */
+ private byte[] generateSalt() {
+ byte[] salt = new byte[SALT_LENGTH_BYTES];
+ new SecureRandom().nextBytes(salt);
+ return salt;
+ }
+
+ /**
+ * Returns {@code true} if {@code credential} looks like a pin.
+ */
+ @VisibleForTesting
+ static boolean isPin(@NonNull String credential) {
+ int length = credential.length();
+ for (int i = 0; i < length; i++) {
+ if (!Character.isDigit(credential.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Hashes {@code credentials} with the given {@code salt}.
+ *
+ * @return The SHA-256 hash.
+ */
+ @VisibleForTesting
+ static byte[] hashCredentials(byte[] salt, String credentials) {
+ byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8);
+ ByteBuffer byteBuffer = ByteBuffer.allocate(
+ salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ byteBuffer.putInt(salt.length);
+ byteBuffer.put(salt);
+ byteBuffer.putInt(credentialsBytes.length);
+ byteBuffer.put(credentialsBytes);
+ byte[] bytes = byteBuffer.array();
+
+ try {
+ return MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
+ } catch (NoSuchAlgorithmException e) {
+ // Impossible, SHA-256 must be supported on Android.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
+ keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
+ return keyGenerator.generateKey();
+ }
+
+ private static List<KeyEntryRecoveryData> createApplicationKeyEntries(
+ Map<String, byte[]> encryptedApplicationKeys) {
+ ArrayList<KeyEntryRecoveryData> keyEntries = new ArrayList<>();
+ for (String alias : encryptedApplicationKeys.keySet()) {
+ keyEntries.add(
+ new KeyEntryRecoveryData(
+ alias.getBytes(StandardCharsets.UTF_8),
+ encryptedApplicationKeys.get(alias)));
+ }
+ return keyEntries;
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java b/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
new file mode 100644
index 00000000..bc080be7
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
@@ -0,0 +1,328 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+/**
+ * Utility functions for the flow where the RecoverableKeyStoreLoader syncs keys with remote
+ * storage.
+ *
+ * @hide
+ */
+public class KeySyncUtils {
+
+ private static final String PUBLIC_KEY_FACTORY_ALGORITHM = "EC";
+ private static final String RECOVERY_KEY_ALGORITHM = "AES";
+ private static final int RECOVERY_KEY_SIZE_BITS = 256;
+
+ private static final byte[] THM_ENCRYPTED_RECOVERY_KEY_HEADER =
+ "V1 THM_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER =
+ "V1 locally_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] ENCRYPTED_APPLICATION_KEY_HEADER =
+ "V1 encrypted_application_key".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] RECOVERY_CLAIM_HEADER =
+ "V1 KF_claim".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] RECOVERY_RESPONSE_HEADER =
+ "V1 reencrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
+
+ private static final byte[] THM_KF_HASH_PREFIX = "THM_KF_hash".getBytes(StandardCharsets.UTF_8);
+
+ private static final int KEY_CLAIMANT_LENGTH_BYTES = 16;
+ private static final int VAULT_PARAMS_LENGTH_BYTES = 85;
+
+ /**
+ * Encrypts the recovery key using both the lock screen hash and the remote storage's public
+ * key.
+ *
+ * @param publicKey The public key of the remote storage.
+ * @param lockScreenHash The user's lock screen hash.
+ * @param vaultParams Additional parameters to send to the remote storage.
+ * @param recoveryKey The recovery key.
+ * @return The encrypted bytes.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+ * @throws InvalidKeyException if the public key or the lock screen could not be used to encrypt
+ * the data.
+ *
+ * @hide
+ */
+ public static byte[] thmEncryptRecoveryKey(
+ PublicKey publicKey,
+ byte[] lockScreenHash,
+ byte[] vaultParams,
+ SecretKey recoveryKey
+ ) throws NoSuchAlgorithmException, InvalidKeyException {
+ byte[] encryptedRecoveryKey = locallyEncryptRecoveryKey(lockScreenHash, recoveryKey);
+ byte[] thmKfHash = calculateThmKfHash(lockScreenHash);
+ byte[] header = concat(THM_ENCRYPTED_RECOVERY_KEY_HEADER, vaultParams);
+ return SecureBox.encrypt(
+ /*theirPublicKey=*/ publicKey,
+ /*sharedSecret=*/ thmKfHash,
+ /*header=*/ header,
+ /*payload=*/ encryptedRecoveryKey);
+ }
+
+ /**
+ * Calculates the THM_KF hash of the lock screen hash.
+ *
+ * @param lockScreenHash The lock screen hash.
+ * @return The hash.
+ * @throws NoSuchAlgorithmException if SHA-256 is unavailable (should never happen).
+ *
+ * @hide
+ */
+ public static byte[] calculateThmKfHash(byte[] lockScreenHash)
+ throws NoSuchAlgorithmException {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ messageDigest.update(THM_KF_HASH_PREFIX);
+ messageDigest.update(lockScreenHash);
+ return messageDigest.digest();
+ }
+
+ /**
+ * Encrypts the recovery key using the lock screen hash.
+ *
+ * @param lockScreenHash The raw lock screen hash.
+ * @param recoveryKey The recovery key.
+ * @return The encrypted bytes.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+ * @throws InvalidKeyException if the hash cannot be used to encrypt for some reason.
+ */
+ @VisibleForTesting
+ static byte[] locallyEncryptRecoveryKey(byte[] lockScreenHash, SecretKey recoveryKey)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ return SecureBox.encrypt(
+ /*theirPublicKey=*/ null,
+ /*sharedSecret=*/ lockScreenHash,
+ /*header=*/ LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER,
+ /*payload=*/ recoveryKey.getEncoded());
+ }
+
+ /**
+ * Returns a new random 256-bit AES recovery key.
+ *
+ * @hide
+ */
+ public static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
+ keyGenerator.init(RECOVERY_KEY_SIZE_BITS, new SecureRandom());
+ return keyGenerator.generateKey();
+ }
+
+ /**
+ * Encrypts all of the given keys with the recovery key, using SecureBox.
+ *
+ * @param recoveryKey The recovery key.
+ * @param keys The keys, indexed by their aliases.
+ * @return The encrypted key material, indexed by aliases.
+ * @throws NoSuchAlgorithmException if any of the SecureBox algorithms are unavailable.
+ * @throws InvalidKeyException if the recovery key is not appropriate for encrypting the keys.
+ *
+ * @hide
+ */
+ public static Map<String, byte[]> encryptKeysWithRecoveryKey(
+ SecretKey recoveryKey, Map<String, SecretKey> keys)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ HashMap<String, byte[]> encryptedKeys = new HashMap<>();
+ for (String alias : keys.keySet()) {
+ SecretKey key = keys.get(alias);
+ byte[] encryptedKey = SecureBox.encrypt(
+ /*theirPublicKey=*/ null,
+ /*sharedSecret=*/ recoveryKey.getEncoded(),
+ /*header=*/ ENCRYPTED_APPLICATION_KEY_HEADER,
+ /*payload=*/ key.getEncoded());
+ encryptedKeys.put(alias, encryptedKey);
+ }
+ return encryptedKeys;
+ }
+
+ /**
+ * Returns a random 16-byte key claimant.
+ *
+ * @hide
+ */
+ public static byte[] generateKeyClaimant() {
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] key = new byte[KEY_CLAIMANT_LENGTH_BYTES];
+ secureRandom.nextBytes(key);
+ return key;
+ }
+
+ /**
+ * Encrypts a claim to recover a remote recovery key.
+ *
+ * @param publicKey The public key of the remote server.
+ * @param vaultParams Associated vault parameters.
+ * @param challenge The challenge issued by the server.
+ * @param thmKfHash The THM hash of the lock screen.
+ * @param keyClaimant The random key claimant.
+ * @return The encrypted recovery claim, to be sent to the remote server.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is not present.
+ * @throws InvalidKeyException if the {@code publicKey} could not be used to encrypt.
+ *
+ * @hide
+ */
+ public static byte[] encryptRecoveryClaim(
+ PublicKey publicKey,
+ byte[] vaultParams,
+ byte[] challenge,
+ byte[] thmKfHash,
+ byte[] keyClaimant) throws NoSuchAlgorithmException, InvalidKeyException {
+ return SecureBox.encrypt(
+ publicKey,
+ /*sharedSecret=*/ null,
+ /*header=*/ concat(RECOVERY_CLAIM_HEADER, vaultParams, challenge),
+ /*payload=*/ concat(thmKfHash, keyClaimant));
+ }
+
+ /**
+ * Decrypts response from recovery claim, returning the locally encrypted key.
+ *
+ * @param keyClaimant The key claimant, used by the remote service to encrypt the response.
+ * @param vaultParams Vault params associated with the claim.
+ * @param encryptedResponse The encrypted response.
+ * @return The locally encrypted recovery key.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is not present.
+ * @throws InvalidKeyException if the {@code keyClaimant} could not be used to decrypt.
+ * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
+ * different key.
+ */
+ public static byte[] decryptRecoveryClaimResponse(
+ byte[] keyClaimant, byte[] vaultParams, byte[] encryptedResponse)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ return SecureBox.decrypt(
+ /*ourPrivateKey=*/ null,
+ /*sharedSecret=*/ keyClaimant,
+ /*header=*/ concat(RECOVERY_RESPONSE_HEADER, vaultParams),
+ /*encryptedPayload=*/ encryptedResponse);
+ }
+
+ /**
+ * Decrypts a recovery key, after having retrieved it from a remote server.
+ *
+ * @param lskfHash The lock screen hash associated with the key.
+ * @param encryptedRecoveryKey The encrypted key.
+ * @return The raw key material.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+ * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
+ * different key.
+ */
+ public static byte[] decryptRecoveryKey(byte[] lskfHash, byte[] encryptedRecoveryKey)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ return SecureBox.decrypt(
+ /*ourPrivateKey=*/ null,
+ /*sharedSecret=*/ lskfHash,
+ /*header=*/ LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER,
+ /*encryptedPayload=*/ encryptedRecoveryKey);
+ }
+
+ /**
+ * Decrypts an application key, using the recovery key.
+ *
+ * @param recoveryKey The recovery key - used to wrap all application keys.
+ * @param encryptedApplicationKey The application key to unwrap.
+ * @return The raw key material of the application key.
+ * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+ * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
+ * different key.
+ */
+ public static byte[] decryptApplicationKey(byte[] recoveryKey, byte[] encryptedApplicationKey)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ return SecureBox.decrypt(
+ /*ourPrivateKey=*/ null,
+ /*sharedSecret=*/ recoveryKey,
+ /*header=*/ ENCRYPTED_APPLICATION_KEY_HEADER,
+ /*encryptedPayload=*/ encryptedApplicationKey);
+ }
+
+ /**
+ * Deserializes a X509 public key.
+ *
+ * @param key The bytes of the key.
+ * @return The key.
+ * @throws NoSuchAlgorithmException if the public key algorithm is unavailable.
+ * @throws InvalidKeySpecException if the bytes of the key are not a valid key.
+ */
+ public static PublicKey deserializePublicKey(byte[] key)
+ throws NoSuchAlgorithmException, InvalidKeySpecException {
+ KeyFactory keyFactory = KeyFactory.getInstance(PUBLIC_KEY_FACTORY_ALGORITHM);
+ X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(key);
+ return keyFactory.generatePublic(publicKeySpec);
+ }
+
+ /**
+ * Packs vault params into a binary format.
+ *
+ * @param thmPublicKey Public key of the trusted hardware module.
+ * @param counterId ID referring to the specific counter in the hardware module.
+ * @param maxAttempts Maximum allowed guesses before trusted hardware wipes key.
+ * @param deviceId ID of the device.
+ * @return The binary vault params, ready for sync.
+ */
+ public static byte[] packVaultParams(
+ PublicKey thmPublicKey, long counterId, int maxAttempts, long deviceId) {
+ return ByteBuffer.allocate(VAULT_PARAMS_LENGTH_BYTES)
+ .order(ByteOrder.LITTLE_ENDIAN)
+ .put(SecureBox.encodePublicKey(thmPublicKey))
+ .putLong(counterId)
+ .putInt(maxAttempts)
+ .putLong(deviceId)
+ .array();
+ }
+
+ /**
+ * Returns the concatenation of all the given {@code arrays}.
+ */
+ @VisibleForTesting
+ static byte[] concat(byte[]... arrays) {
+ int length = 0;
+ for (byte[] array : arrays) {
+ length += array.length;
+ }
+
+ byte[] concatenated = new byte[length];
+ int pos = 0;
+ for (byte[] array : arrays) {
+ System.arraycopy(array, /*srcPos=*/ 0, concatenated, pos, array.length);
+ pos += array.length;
+ }
+
+ return concatenated;
+ }
+
+ // Statics only
+ private KeySyncUtils() {}
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/PlatformDecryptionKey.java b/com/android/server/locksettings/recoverablekeystore/PlatformDecryptionKey.java
new file mode 100644
index 00000000..35571f1f
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/PlatformDecryptionKey.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+import android.security.keystore.AndroidKeyStoreSecretKey;
+
+/**
+ * Used to unwrap recoverable keys before syncing them with remote storage.
+ *
+ * <p>This is a private key stored in AndroidKeyStore. Has an associated generation ID, which is
+ * stored with wrapped keys, allowing us to ensure the wrapped key has the same version as the
+ * platform key.
+ *
+ * @hide
+ */
+public class PlatformDecryptionKey {
+
+ private final int mGenerationId;
+ private final AndroidKeyStoreSecretKey mKey;
+
+ /**
+ * A new instance.
+ *
+ * @param generationId The generation ID of the platform key.
+ * @param key The key handle in AndroidKeyStore.
+ *
+ * @hide
+ */
+ public PlatformDecryptionKey(int generationId, AndroidKeyStoreSecretKey key) {
+ mGenerationId = generationId;
+ mKey = key;
+ }
+
+ /**
+ * Returns the generation ID.
+ *
+ * @hide
+ */
+ public int getGenerationId() {
+ return mGenerationId;
+ }
+
+ /**
+ * Returns the actual key, which can be used to decrypt.
+ *
+ * @hide
+ */
+ public AndroidKeyStoreSecretKey getKey() {
+ return mKey;
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/PlatformEncryptionKey.java b/com/android/server/locksettings/recoverablekeystore/PlatformEncryptionKey.java
new file mode 100644
index 00000000..38f5b45e
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/PlatformEncryptionKey.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 com.android.server.locksettings.recoverablekeystore;
+
+import android.security.keystore.AndroidKeyStoreSecretKey;
+
+/**
+ * Private key stored in AndroidKeyStore. Used to wrap recoverable keys before writing them to disk.
+ *
+ * <p>Identified by a generation ID, which increments whenever a new platform key is generated. A
+ * new key must be generated whenever the user disables their lock screen, as the decryption key is
+ * tied to lock-screen authentication.
+ *
+ * <p>One current platform key exists per profile on the device. (As each must be tied to a
+ * different user's lock screen.)
+ *
+ * @hide
+ */
+public class PlatformEncryptionKey {
+
+ private final int mGenerationId;
+ private final AndroidKeyStoreSecretKey mKey;
+
+ /**
+ * A new instance.
+ *
+ * @param generationId The generation ID of the key.
+ * @param key The secret key handle. Can be used to encrypt WITHOUT requiring screen unlock.
+ */
+ public PlatformEncryptionKey(int generationId, AndroidKeyStoreSecretKey key) {
+ mGenerationId = generationId;
+ mKey = key;
+ }
+
+ /**
+ * Returns the generation ID of the key.
+ */
+ public int getGenerationId() {
+ return mGenerationId;
+ }
+
+ /**
+ * Returns the actual key, which can only be used to encrypt.
+ */
+ public AndroidKeyStoreSecretKey getKey() {
+ return mKey;
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java b/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
new file mode 100644
index 00000000..95f5cb7a
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
@@ -0,0 +1,348 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.security.keystore.AndroidKeyStoreSecretKey;
+import android.security.keystore.KeyProperties;
+import android.security.keystore.KeyProtection;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Locale;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.security.auth.DestroyFailedException;
+
+/**
+ * Manages creating and checking the validity of the platform key.
+ *
+ * <p>The platform key is used to wrap the material of recoverable keys before persisting them to
+ * disk. It is also used to decrypt the same keys on a screen unlock, before re-wrapping them with
+ * a recovery key and syncing them with remote storage.
+ *
+ * <p>Each platform key has two entries in AndroidKeyStore:
+ *
+ * <ul>
+ * <li>Encrypt entry - this entry enables the root user to at any time encrypt.
+ * <li>Decrypt entry - this entry enables the root user to decrypt only after recent user
+ * authentication, i.e., within 15 seconds after a screen unlock.
+ * </ul>
+ *
+ * <p>Both entries are enabled only for AES/GCM/NoPadding Cipher algorithm.
+ *
+ * @hide
+ */
+public class PlatformKeyManager {
+ private static final String TAG = "PlatformKeyManager";
+
+ private static final String KEY_ALGORITHM = "AES";
+ private static final int KEY_SIZE_BITS = 256;
+ private static final String KEY_ALIAS_PREFIX =
+ "com.android.server.locksettings.recoverablekeystore/platform/";
+ private static final String ENCRYPT_KEY_ALIAS_SUFFIX = "encrypt";
+ private static final String DECRYPT_KEY_ALIAS_SUFFIX = "decrypt";
+ private static final int USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS = 15;
+
+ private final Context mContext;
+ private final KeyStoreProxy mKeyStore;
+ private final RecoverableKeyStoreDb mDatabase;
+ private final int mUserId;
+
+ private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
+
+ /**
+ * A new instance operating on behalf of {@code userId}, storing its prefs in the location
+ * defined by {@code context}.
+ *
+ * @param context This should be the context of the RecoverableKeyStoreLoader service.
+ * @param userId The ID of the user to whose lock screen the platform key must be bound.
+ * @throws KeyStoreException if failed to initialize AndroidKeyStore.
+ * @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
+ * @throws InsecureUserException if the user does not have a lock screen set.
+ * @throws SecurityException if the caller does not have permission to write to /data/system.
+ *
+ * @hide
+ */
+ public static PlatformKeyManager getInstance(Context context, RecoverableKeyStoreDb database, int userId)
+ throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException {
+ context = context.getApplicationContext();
+ PlatformKeyManager keyManager = new PlatformKeyManager(
+ userId,
+ context,
+ new KeyStoreProxyImpl(getAndLoadAndroidKeyStore()),
+ database);
+ keyManager.init();
+ return keyManager;
+ }
+
+ @VisibleForTesting
+ PlatformKeyManager(
+ int userId,
+ Context context,
+ KeyStoreProxy keyStore,
+ RecoverableKeyStoreDb database) {
+ mUserId = userId;
+ mKeyStore = keyStore;
+ mContext = context;
+ mDatabase = database;
+ }
+
+ /**
+ * Returns the current generation ID of the platform key. This increments whenever a platform
+ * key has to be replaced. (e.g., because the user has removed and then re-added their lock
+ * screen).
+ *
+ * @hide
+ */
+ public int getGenerationId() {
+ int generationId = mDatabase.getPlatformKeyGenerationId(mUserId);
+ if (generationId == -1) {
+ return 1;
+ }
+ return generationId;
+ }
+
+ /**
+ * Returns {@code true} if the platform key is available. A platform key won't be available if
+ * the user has not set up a lock screen.
+ *
+ * @hide
+ */
+ public boolean isAvailable() {
+ return mContext.getSystemService(KeyguardManager.class).isDeviceSecure(mUserId);
+ }
+
+ /**
+ * Generates a new key and increments the generation ID. Should be invoked if the platform key
+ * is corrupted and needs to be rotated.
+ *
+ * @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
+ * @throws KeyStoreException if there is an error in AndroidKeyStore.
+ *
+ * @hide
+ */
+ public void regenerate() throws NoSuchAlgorithmException, KeyStoreException {
+ int nextId = getGenerationId() + 1;
+ generateAndLoadKey(nextId);
+ setGenerationId(nextId);
+ }
+
+ /**
+ * Returns the platform key used for encryption.
+ *
+ * @throws KeyStoreException if there was an AndroidKeyStore error.
+ * @throws UnrecoverableKeyException if the key could not be recovered.
+ * @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
+ *
+ * @hide
+ */
+ public PlatformEncryptionKey getEncryptKey()
+ throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
+ int generationId = getGenerationId();
+ AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
+ getEncryptAlias(generationId), /*password=*/ null);
+ return new PlatformEncryptionKey(generationId, key);
+ }
+
+ /**
+ * Returns the platform key used for decryption. Only works after a recent screen unlock.
+ *
+ * @throws KeyStoreException if there was an AndroidKeyStore error.
+ * @throws UnrecoverableKeyException if the key could not be recovered.
+ * @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
+ *
+ * @hide
+ */
+ public PlatformDecryptionKey getDecryptKey()
+ throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
+ int generationId = getGenerationId();
+ AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
+ getDecryptAlias(generationId), /*password=*/ null);
+ return new PlatformDecryptionKey(generationId, key);
+ }
+
+ /**
+ * Initializes the class. If there is no current platform key, and the user has a lock screen
+ * set, will create the platform key and set the generation ID.
+ *
+ * @throws KeyStoreException if there was an error in AndroidKeyStore.
+ * @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
+ *
+ * @hide
+ */
+ public void init() throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException {
+ if (!isAvailable()) {
+ throw new InsecureUserException(String.format(
+ Locale.US, "%d does not have a lock screen set.", mUserId));
+ }
+
+ int generationId = getGenerationId();
+ if (isKeyLoaded(generationId)) {
+ Log.i(TAG, String.format(
+ Locale.US, "Platform key generation %d exists already.", generationId));
+ return;
+ }
+ if (generationId == 1) {
+ Log.i(TAG, "Generating initial platform ID.");
+ } else {
+ Log.w(TAG, String.format(Locale.US, "Platform generation ID was %d but no "
+ + "entry was present in AndroidKeyStore. Generating fresh key.", generationId));
+ }
+
+ generateAndLoadKey(generationId);
+ }
+
+ /**
+ * Returns the alias of the encryption key with the specific {@code generationId} in the
+ * AndroidKeyStore.
+ *
+ * <p>These IDs look as follows:
+ * {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/encrypt}
+ *
+ * @param generationId The generation ID.
+ * @return The alias.
+ */
+ private String getEncryptAlias(int generationId) {
+ return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + ENCRYPT_KEY_ALIAS_SUFFIX;
+ }
+
+ /**
+ * Returns the alias of the decryption key with the specific {@code generationId} in the
+ * AndroidKeyStore.
+ *
+ * <p>These IDs look as follows:
+ * {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/decrypt}
+ *
+ * @param generationId The generation ID.
+ * @return The alias.
+ */
+ private String getDecryptAlias(int generationId) {
+ return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + DECRYPT_KEY_ALIAS_SUFFIX;
+ }
+
+ /**
+ * Sets the current generation ID to {@code generationId}.
+ */
+ private void setGenerationId(int generationId) {
+ mDatabase.setPlatformKeyGenerationId(mUserId, generationId);
+ }
+
+ /**
+ * Returns {@code true} if a key has been loaded with the given {@code generationId} into
+ * AndroidKeyStore.
+ *
+ * @throws KeyStoreException if there was an error checking AndroidKeyStore.
+ */
+ private boolean isKeyLoaded(int generationId) throws KeyStoreException {
+ return mKeyStore.containsAlias(getEncryptAlias(generationId))
+ && mKeyStore.containsAlias(getDecryptAlias(generationId));
+ }
+
+ /**
+ * Generates a new 256-bit AES key, and loads it into AndroidKeyStore with the given
+ * {@code generationId} determining its aliases.
+ *
+ * @throws NoSuchAlgorithmException if AES is unavailable. This should never happen, as it is
+ * available since API version 1.
+ * @throws KeyStoreException if there was an issue loading the keys into AndroidKeyStore.
+ */
+ private void generateAndLoadKey(int generationId)
+ throws NoSuchAlgorithmException, KeyStoreException {
+ String encryptAlias = getEncryptAlias(generationId);
+ String decryptAlias = getDecryptAlias(generationId);
+ SecretKey secretKey = generateAesKey();
+
+ mKeyStore.setEntry(
+ encryptAlias,
+ new KeyStore.SecretKeyEntry(secretKey),
+ new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .build());
+ mKeyStore.setEntry(
+ decryptAlias,
+ new KeyStore.SecretKeyEntry(secretKey),
+ new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
+ .setUserAuthenticationRequired(true)
+ .setUserAuthenticationValidityDurationSeconds(
+ USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .setBoundToSpecificSecureUserId(mUserId)
+ .build());
+
+ try {
+ secretKey.destroy();
+ } catch (DestroyFailedException e) {
+ Log.w(TAG, "Failed to destroy in-memory platform key.", e);
+ }
+ }
+
+ /**
+ * Generates a new 256-bit AES key, in software.
+ *
+ * @return The software-generated AES key.
+ * @throws NoSuchAlgorithmException if AES key generation is not available. This should never
+ * happen, as AES has been supported since API level 1.
+ */
+ private static SecretKey generateAesKey() throws NoSuchAlgorithmException {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
+ keyGenerator.init(KEY_SIZE_BITS);
+ return keyGenerator.generateKey();
+ }
+
+ /**
+ * Returns AndroidKeyStore-provided {@link KeyStore}, having already invoked
+ * {@link KeyStore#load(KeyStore.LoadStoreParameter)}.
+ *
+ * @throws KeyStoreException if there was a problem getting or initializing the key store.
+ */
+ private static KeyStore getAndLoadAndroidKeyStore() throws KeyStoreException {
+ KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER);
+ try {
+ keyStore.load(/*param=*/ null);
+ } catch (CertificateException | IOException | NoSuchAlgorithmException e) {
+ // Should never happen.
+ throw new KeyStoreException("Unable to load keystore.", e);
+ }
+ return keyStore;
+ }
+
+ /**
+ * @hide
+ */
+ public interface Factory {
+ /**
+ * New PlatformKeyManager instance.
+ *
+ * @hide
+ */
+ PlatformKeyManager newInstance()
+ throws NoSuchAlgorithmException, InsecureUserException, KeyStoreException;
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
new file mode 100644
index 00000000..8c23d9b4
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
@@ -0,0 +1,104 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
+
+import java.security.InvalidKeyException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Locale;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+/**
+ * Generates keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
+ *
+ * <p>Generates 256-bit AES keys, which can be used for encrypt / decrypt with AES/GCM/NoPadding.
+ * They are synced to disk wrapped by a platform key. This allows them to be exported to a remote
+ * service.
+ *
+ * @hide
+ */
+public class RecoverableKeyGenerator {
+ private static final int RESULT_CANNOT_INSERT_ROW = -1;
+ private static final String KEY_GENERATOR_ALGORITHM = "AES";
+ private static final int KEY_SIZE_BITS = 256;
+
+ /**
+ * A new {@link RecoverableKeyGenerator} instance.
+ *
+ * @throws NoSuchAlgorithmException if "AES" key generation or "AES/GCM/NoPadding" cipher is
+ * unavailable. Should never happen.
+ *
+ * @hide
+ */
+ public static RecoverableKeyGenerator newInstance(RecoverableKeyStoreDb database)
+ throws NoSuchAlgorithmException {
+ // NB: This cannot use AndroidKeyStore as the provider, as we need access to the raw key
+ // material, so that it can be synced to disk in encrypted form.
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_GENERATOR_ALGORITHM);
+ return new RecoverableKeyGenerator(keyGenerator, database);
+ }
+
+ private final KeyGenerator mKeyGenerator;
+ private final RecoverableKeyStoreDb mDatabase;
+
+ private RecoverableKeyGenerator(
+ KeyGenerator keyGenerator,
+ RecoverableKeyStoreDb recoverableKeyStoreDb) {
+ mKeyGenerator = keyGenerator;
+ mDatabase = recoverableKeyStoreDb;
+ }
+
+ /**
+ * Generates a 256-bit AES key with the given alias.
+ *
+ * <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
+ * persisted to disk so that it can be synced remotely, and then recovered on another device.
+ * The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
+ *
+ * @param platformKey The user's platform key, with which to wrap the generated key.
+ * @param userId The user ID of the profile to which the calling app belongs.
+ * @param uid The uid of the application that will own the key.
+ * @param alias The alias by which the key will be known in the recoverable key store.
+ * @throws RecoverableKeyStorageException if there is some error persisting the key either to
+ * the database.
+ * @throws KeyStoreException if there is a KeyStore error wrapping the generated key.
+ * @throws InvalidKeyException if the platform key cannot be used to wrap keys.
+ *
+ * @hide
+ */
+ public byte[] generateAndStoreKey(
+ PlatformEncryptionKey platformKey, int userId, int uid, String alias)
+ throws RecoverableKeyStorageException, KeyStoreException, InvalidKeyException {
+ mKeyGenerator.init(KEY_SIZE_BITS);
+ SecretKey key = mKeyGenerator.generateKey();
+
+ WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, key);
+ long result = mDatabase.insertKey(userId, uid, alias, wrappedKey);
+
+ if (result == RESULT_CANNOT_INSERT_ROW) {
+ throw new RecoverableKeyStorageException(
+ String.format(
+ Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
+ }
+
+ return key.getEncoded();
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageException.java b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageException.java
new file mode 100644
index 00000000..f9d28f17
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageException.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+/**
+ * Error thrown when there was a problem writing or reading recoverable key information to or from
+ * storage.
+ *
+ * <p>Storage is typically
+ * {@link com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb} or
+ * AndroidKeyStore.
+ */
+public class RecoverableKeyStorageException extends Exception {
+ public RecoverableKeyStorageException(String message) {
+ super(message);
+ }
+
+ public RecoverableKeyStorageException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
new file mode 100644
index 00000000..fe1cad4b
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -0,0 +1,506 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+
+import android.security.recoverablekeystore.KeyEntryRecoveryData;
+import android.security.recoverablekeystore.KeyStoreRecoveryData;
+import android.security.recoverablekeystore.KeyStoreRecoveryMetadata;
+import android.security.recoverablekeystore.RecoverableKeyStoreLoader;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.KeyStoreException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.UnrecoverableKeyException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.crypto.AEADBadTagException;
+
+/**
+ * Class with {@link RecoverableKeyStoreLoader} API implementation and internal methods to interact
+ * with {@code LockSettingsService}.
+ *
+ * @hide
+ */
+public class RecoverableKeyStoreManager {
+ private static final String TAG = "RecoverableKeyStoreMgr";
+
+ private static final int ERROR_INSECURE_USER = 1;
+ private static final int ERROR_KEYSTORE_INTERNAL_ERROR = 2;
+ private static final int ERROR_DATABASE_ERROR = 3;
+
+ private static RecoverableKeyStoreManager mInstance;
+
+ private final Context mContext;
+ private final RecoverableKeyStoreDb mDatabase;
+ private final RecoverySessionStorage mRecoverySessionStorage;
+ private final ExecutorService mExecutorService;
+ private final RecoverySnapshotListenersStorage mListenersStorage;
+ private final RecoverableKeyGenerator mRecoverableKeyGenerator;
+ private final RecoverySnapshotStorage mSnapshotStorage;
+
+ /**
+ * Returns a new or existing instance.
+ *
+ * @hide
+ */
+ public static synchronized RecoverableKeyStoreManager getInstance(Context mContext) {
+ if (mInstance == null) {
+ RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(mContext);
+ mInstance = new RecoverableKeyStoreManager(
+ mContext.getApplicationContext(),
+ db,
+ new RecoverySessionStorage(),
+ Executors.newSingleThreadExecutor(),
+ new RecoverySnapshotStorage(),
+ new RecoverySnapshotListenersStorage());
+ }
+ return mInstance;
+ }
+
+ @VisibleForTesting
+ RecoverableKeyStoreManager(
+ Context context,
+ RecoverableKeyStoreDb recoverableKeyStoreDb,
+ RecoverySessionStorage recoverySessionStorage,
+ ExecutorService executorService,
+ RecoverySnapshotStorage snapshotStorage,
+ RecoverySnapshotListenersStorage listenersStorage) {
+ mContext = context;
+ mDatabase = recoverableKeyStoreDb;
+ mRecoverySessionStorage = recoverySessionStorage;
+ mExecutorService = executorService;
+ mListenersStorage = listenersStorage;
+ mSnapshotStorage = snapshotStorage;
+ try {
+ mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase);
+ } catch (NoSuchAlgorithmException e) {
+ // Impossible: all AOSP implementations must support AES.
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void initRecoveryService(
+ @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList, int userId)
+ throws RemoteException {
+ checkRecoverKeyStorePermission();
+ // TODO: open /system/etc/security/... cert file, and check the signature on the public keys
+ PublicKey publicKey;
+ try {
+ KeyFactory kf = KeyFactory.getInstance("EC");
+ // TODO: Randomly choose a key from the list -- right now we just use the whole input
+ X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(signedPublicKeyList);
+ publicKey = kf.generatePublic(pkSpec);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen
+ throw new RuntimeException(e);
+ } catch (InvalidKeySpecException e) {
+ throw new RemoteException("Invalid public key for the recovery service");
+ }
+ mDatabase.setRecoveryServicePublicKey(userId, Binder.getCallingUid(), publicKey);
+ }
+
+ /**
+ * Gets all data necessary to recover application keys on new device.
+ *
+ * @return recovery data
+ * @hide
+ */
+ public @NonNull KeyStoreRecoveryData getRecoveryData(@NonNull byte[] account, int userId)
+ throws RemoteException {
+ checkRecoverKeyStorePermission();
+
+ KeyStoreRecoveryData snapshot = mSnapshotStorage.get(UserHandle.getCallingUserId());
+ if (snapshot == null) {
+ throw new ServiceSpecificException(RecoverableKeyStoreLoader.NO_SNAPSHOT_PENDING_ERROR);
+ }
+ return snapshot;
+ }
+
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent, int userId)
+ throws RemoteException {
+ checkRecoverKeyStorePermission();
+ final int recoveryAgentUid = Binder.getCallingUid();
+ mListenersStorage.setSnapshotListener(recoveryAgentUid, intent);
+ }
+
+ /**
+ * Gets recovery snapshot versions for all accounts. Note that snapshot may have 0 application
+ * keys, but it still needs to be synced, if previous versions were not empty.
+ *
+ * @return Map from Recovery agent account to snapshot version.
+ */
+ public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions(int userId)
+ throws RemoteException {
+ checkRecoverKeyStorePermission();
+ throw new UnsupportedOperationException();
+ }
+
+ public void setServerParameters(long serverParameters, int userId) throws RemoteException {
+ checkRecoverKeyStorePermission();
+ mDatabase.setServerParameters(userId, Binder.getCallingUid(), serverParameters);
+ }
+
+ /**
+ * Updates recovery status for the application given its {@code packageName}.
+ *
+ * @param packageName which recoverable key statuses will be returned
+ * @param aliases - KeyStore aliases or {@code null} for all aliases of the app
+ * @param status - new status
+ */
+ public void setRecoveryStatus(
+ @NonNull String packageName, @Nullable String[] aliases, int status, int userId)
+ throws RemoteException {
+ checkRecoverKeyStorePermission();
+ int uid = Binder.getCallingUid();
+ if (packageName != null) {
+ // TODO: get uid for package name, when many apps are supported.
+ }
+ if (aliases == null) {
+ // Get all keys for the app.
+ Map<String, Integer> allKeys = mDatabase.getStatusForAllKeys(uid);
+ aliases = new String[allKeys.size()];
+ allKeys.keySet().toArray(aliases);
+ }
+ for (String alias: aliases) {
+ mDatabase.setRecoveryStatus(uid, alias, status);
+ }
+ }
+
+ /**
+ * Gets recovery status for caller or other application {@code packageName}.
+ * @param packageName which recoverable keys statuses will be returned.
+ *
+ * @return {@code Map} from KeyStore alias to recovery status.
+ */
+ public @NonNull Map<String, Integer> getRecoveryStatus(@Nullable String packageName, int userId)
+ throws RemoteException {
+ // Any application should be able to check status for its own keys.
+ // If caller is a recovery agent it can check statuses for other packages, but
+ // only for recoverable keys it manages.
+ checkRecoverKeyStorePermission();
+ return mDatabase.getStatusForAllKeys(Binder.getCallingUid());
+ }
+
+ /**
+ * Sets recovery secrets list used by all recovery agents for given {@code userId}
+ *
+ * @hide
+ */
+ public void setRecoverySecretTypes(
+ @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] secretTypes, int userId)
+ throws RemoteException {
+ checkRecoverKeyStorePermission();
+ mDatabase.setRecoverySecretTypes(UserHandle.getCallingUserId(), Binder.getCallingUid(),
+ secretTypes);
+ }
+
+ /**
+ * Gets secret types necessary to create Recovery Data.
+ *
+ * @return secret types
+ * @hide
+ */
+ public @NonNull int[] getRecoverySecretTypes(int userId) throws RemoteException {
+ checkRecoverKeyStorePermission();
+ return mDatabase.getRecoverySecretTypes(UserHandle.getCallingUserId(),
+ Binder.getCallingUid());
+ }
+
+ /**
+ * Gets secret types RecoverableKeyStoreLoaders is waiting for to create new Recovery Data.
+ *
+ * @return secret types
+ * @hide
+ */
+ public @NonNull int[] getPendingRecoverySecretTypes(int userId) throws RemoteException {
+ checkRecoverKeyStorePermission();
+ throw new UnsupportedOperationException();
+ }
+
+ public void recoverySecretAvailable(
+ @NonNull KeyStoreRecoveryMetadata recoverySecret, int userId) throws RemoteException {
+ final int callingUid = Binder.getCallingUid(); // Recovery agent uid.
+ if (recoverySecret.getLockScreenUiFormat() == KeyStoreRecoveryMetadata.TYPE_LOCKSCREEN) {
+ throw new SecurityException(
+ "Caller " + callingUid + "is not allowed to set lock screen secret");
+ }
+ checkRecoverKeyStorePermission();
+ // TODO: add hook from LockSettingsService to set lock screen secret.
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Initializes recovery session.
+ *
+ * @param sessionId A unique ID to identify the recovery session.
+ * @param verifierPublicKey X509-encoded public key.
+ * @param vaultParams Additional params associated with vault.
+ * @param vaultChallenge Challenge issued by vault service.
+ * @param secrets Lock-screen hashes. For now only a single secret is supported.
+ * @return Encrypted bytes of recovery claim. This can then be issued to the vault service.
+ *
+ * @hide
+ */
+ public @NonNull byte[] startRecoverySession(
+ @NonNull String sessionId,
+ @NonNull byte[] verifierPublicKey,
+ @NonNull byte[] vaultParams,
+ @NonNull byte[] vaultChallenge,
+ @NonNull List<KeyStoreRecoveryMetadata> secrets,
+ int userId)
+ throws RemoteException {
+ checkRecoverKeyStorePermission();
+
+ if (secrets.size() != 1) {
+ // TODO: support multiple secrets
+ throw new RemoteException("Only a single KeyStoreRecoveryMetadata is supported");
+ }
+
+ byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
+ byte[] kfHash = secrets.get(0).getSecret();
+ mRecoverySessionStorage.add(
+ userId,
+ new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant, vaultParams));
+
+ try {
+ byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(kfHash);
+ PublicKey publicKey = KeySyncUtils.deserializePublicKey(verifierPublicKey);
+ return KeySyncUtils.encryptRecoveryClaim(
+ publicKey,
+ vaultParams,
+ vaultChallenge,
+ thmKfHash,
+ keyClaimant);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen: all the algorithms used are required by AOSP implementations.
+ throw new RemoteException(
+ "Missing required algorithm",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ } catch (InvalidKeySpecException | InvalidKeyException e) {
+ throw new RemoteException(
+ "Not a valid X509 key",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ }
+ }
+
+ /**
+ * Invoked by a recovery agent after a successful recovery claim is sent to the remote vault
+ * service.
+ *
+ * @param sessionId The session ID used to generate the claim. See
+ * {@link #startRecoverySession(String, byte[], byte[], byte[], List, int)}.
+ * @param encryptedRecoveryKey The encrypted recovery key blob returned by the remote vault
+ * service.
+ * @param applicationKeys The encrypted key blobs returned by the remote vault service. These
+ * were wrapped with the recovery key.
+ * @param uid The uid of the recovery agent.
+ * @return Map from alias to raw key material.
+ * @throws RemoteException if an error occurred recovering the keys.
+ */
+ public Map<String, byte[]> recoverKeys(
+ @NonNull String sessionId,
+ @NonNull byte[] encryptedRecoveryKey,
+ @NonNull List<KeyEntryRecoveryData> applicationKeys,
+ int uid)
+ throws RemoteException {
+ checkRecoverKeyStorePermission();
+
+ RecoverySessionStorage.Entry sessionEntry = mRecoverySessionStorage.get(uid, sessionId);
+ if (sessionEntry == null) {
+ throw new RemoteException(String.format(Locale.US,
+ "User %d does not have pending session '%s'", uid, sessionId));
+ }
+
+ try {
+ byte[] recoveryKey = decryptRecoveryKey(sessionEntry, encryptedRecoveryKey);
+ return recoverApplicationKeys(recoveryKey, applicationKeys);
+ } finally {
+ sessionEntry.destroy();
+ mRecoverySessionStorage.remove(uid);
+ }
+ }
+
+ /**
+ * Generates a key named {@code alias} in the recoverable store for the calling uid. Then
+ * returns the raw key material.
+ *
+ * <p>TODO: Once AndroidKeyStore has added move api, do not return raw bytes.
+ *
+ * @hide
+ */
+ public byte[] generateAndStoreKey(@NonNull String alias) throws RemoteException {
+ int uid = Binder.getCallingUid();
+ int userId = Binder.getCallingUserHandle().getIdentifier();
+
+ PlatformEncryptionKey encryptionKey;
+
+ try {
+ PlatformKeyManager platformKeyManager = PlatformKeyManager.getInstance(
+ mContext, mDatabase, userId);
+ encryptionKey = platformKeyManager.getEncryptKey();
+ } catch (NoSuchAlgorithmException e) {
+ // Impossible: all algorithms must be supported by AOSP
+ throw new RuntimeException(e);
+ } catch (KeyStoreException | UnrecoverableKeyException e) {
+ throw new ServiceSpecificException(ERROR_KEYSTORE_INTERNAL_ERROR, e.getMessage());
+ } catch (InsecureUserException e) {
+ throw new ServiceSpecificException(ERROR_INSECURE_USER, e.getMessage());
+ }
+
+ try {
+ return mRecoverableKeyGenerator.generateAndStoreKey(encryptionKey, userId, uid, alias);
+ } catch (KeyStoreException | InvalidKeyException e) {
+ throw new ServiceSpecificException(ERROR_KEYSTORE_INTERNAL_ERROR, e.getMessage());
+ } catch (RecoverableKeyStorageException e) {
+ throw new ServiceSpecificException(ERROR_DATABASE_ERROR, e.getMessage());
+ }
+ }
+
+ private byte[] decryptRecoveryKey(
+ RecoverySessionStorage.Entry sessionEntry, byte[] encryptedClaimResponse)
+ throws RemoteException {
+ try {
+ byte[] locallyEncryptedKey = KeySyncUtils.decryptRecoveryClaimResponse(
+ sessionEntry.getKeyClaimant(),
+ sessionEntry.getVaultParams(),
+ encryptedClaimResponse);
+ return KeySyncUtils.decryptRecoveryKey(sessionEntry.getLskfHash(), locallyEncryptedKey);
+ } catch (InvalidKeyException | AEADBadTagException e) {
+ throw new RemoteException(
+ "Failed to decrypt recovery key",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen: all the algorithms used are required by AOSP implementations
+ throw new RemoteException(
+ "Missing required algorithm",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ }
+ }
+
+ /**
+ * Uses {@code recoveryKey} to decrypt {@code applicationKeys}.
+ *
+ * @return Map from alias to raw key material.
+ * @throws RemoteException if an error occurred decrypting the keys.
+ */
+ private Map<String, byte[]> recoverApplicationKeys(
+ @NonNull byte[] recoveryKey,
+ @NonNull List<KeyEntryRecoveryData> applicationKeys) throws RemoteException {
+ HashMap<String, byte[]> keyMaterialByAlias = new HashMap<>();
+ for (KeyEntryRecoveryData applicationKey : applicationKeys) {
+ String alias = new String(applicationKey.getAlias(), StandardCharsets.UTF_8);
+ byte[] encryptedKeyMaterial = applicationKey.getEncryptedKeyMaterial();
+
+ try {
+ byte[] keyMaterial =
+ KeySyncUtils.decryptApplicationKey(recoveryKey, encryptedKeyMaterial);
+ keyMaterialByAlias.put(alias, keyMaterial);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen: all the algorithms used are required by AOSP implementations
+ throw new RemoteException(
+ "Missing required algorithm",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ } catch (InvalidKeyException | AEADBadTagException e) {
+ throw new RemoteException(
+ "Failed to recover key with alias '" + alias + "'",
+ e,
+ /*enableSuppression=*/ true,
+ /*writeableStackTrace=*/ true);
+ }
+ }
+ return keyMaterialByAlias;
+ }
+
+ /**
+ * This function can only be used inside LockSettingsService.
+ *
+ * @param storedHashType from {@code CredentialHash}
+ * @param credential - unencrypted String. Password length should be at most 16 symbols {@code
+ * mPasswordMaxLength}
+ * @param userId for user who just unlocked the device.
+ * @hide
+ */
+ public void lockScreenSecretAvailable(
+ int storedHashType, @NonNull String credential, int userId) {
+ // So as not to block the critical path unlocking the phone, defer to another thread.
+ try {
+ mExecutorService.execute(KeySyncTask.newInstance(
+ mContext,
+ mDatabase,
+ mSnapshotStorage,
+ mListenersStorage,
+ userId,
+ storedHashType,
+ credential));
+ } catch (NoSuchAlgorithmException e) {
+ Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e);
+ } catch (KeyStoreException e) {
+ Log.e(TAG, "Key store error encountered during recoverable key sync", e);
+ } catch (InsecureUserException e) {
+ Log.wtf(TAG, "Impossible - insecure user, but user just entered lock screen", e);
+ }
+ }
+
+ /** This function can only be used inside LockSettingsService. */
+ public void lockScreenSecretChanged(
+ @KeyStoreRecoveryMetadata.LockScreenUiFormat int type,
+ @Nullable String credential,
+ int userId) {
+ throw new UnsupportedOperationException();
+ }
+
+ private void checkRecoverKeyStorePermission() {
+ mContext.enforceCallingOrSelfPermission(
+ RecoverableKeyStoreLoader.PERMISSION_RECOVER_KEYSTORE,
+ "Caller " + Binder.getCallingUid() + " doesn't have RecoverKeyStore permission.");
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java b/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java
new file mode 100644
index 00000000..c925329e
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.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 com.android.server.locksettings.recoverablekeystore;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * In memory storage for listeners to be notified when new recovery snapshot is available. This
+ * class is thread-safe. It is used on two threads - the service thread and the thread that runs the
+ * {@link KeySyncTask}.
+ *
+ * @hide
+ */
+public class RecoverySnapshotListenersStorage {
+ private static final String TAG = "RecoverySnapshotLstnrs";
+
+ @GuardedBy("this")
+ private SparseArray<PendingIntent> mAgentIntents = new SparseArray<>();
+
+ /**
+ * Sets new listener for the recovery agent, identified by {@code uid}.
+ *
+ * @param recoveryAgentUid uid of the recovery agent.
+ * @param intent PendingIntent which will be triggered when new snapshot is available.
+ */
+ public synchronized void setSnapshotListener(
+ int recoveryAgentUid, @Nullable PendingIntent intent) {
+ Log.i(TAG, "Registered listener for agent with uid " + recoveryAgentUid);
+ mAgentIntents.put(recoveryAgentUid, intent);
+ }
+
+ /**
+ * Returns {@code true} if a listener has been set for the recovery agent.
+ */
+ public synchronized boolean hasListener(int recoveryAgentUid) {
+ return mAgentIntents.get(recoveryAgentUid) != null;
+ }
+
+ /**
+ * Notifies recovery agent that new snapshot is available. Does nothing if a listener was not
+ * registered.
+ *
+ * @param recoveryAgentUid uid of recovery agent.
+ */
+ public synchronized void recoverySnapshotAvailable(int recoveryAgentUid) {
+ PendingIntent intent = mAgentIntents.get(recoveryAgentUid);
+ if (intent != null) {
+ try {
+ intent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG,
+ "Failed to trigger PendingIntent for " + recoveryAgentUid,
+ e);
+ }
+ }
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/SecureBox.java b/com/android/server/locksettings/recoverablekeystore/SecureBox.java
new file mode 100644
index 00000000..807ee034
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/SecureBox.java
@@ -0,0 +1,471 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore;
+
+import android.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
+import java.math.BigInteger;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.EllipticCurve;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import javax.crypto.AEADBadTagException;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyAgreement;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Implementation of the SecureBox v2 crypto functions.
+ *
+ * <p>Securebox v2 provides a simple interface to perform encryptions by using any of the following
+ * credential types:
+ *
+ * <ul>
+ * <li>A public key owned by the recipient,
+ * <li>A secret shared between the sender and the recipient, or
+ * <li>Both a recipient's public key and a shared secret.
+ * </ul>
+ *
+ * @hide
+ */
+public class SecureBox {
+
+ private static final byte[] VERSION = new byte[] {(byte) 0x02, 0}; // LITTLE_ENDIAN_TWO_BYTES(2)
+ private static final byte[] HKDF_SALT =
+ concat("SECUREBOX".getBytes(StandardCharsets.UTF_8), VERSION);
+ private static final byte[] HKDF_INFO_WITH_PUBLIC_KEY =
+ "P256 HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] HKDF_INFO_WITHOUT_PUBLIC_KEY =
+ "SHARED HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] CONSTANT_01 = {(byte) 0x01};
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+ private static final byte EC_PUBLIC_KEY_PREFIX = (byte) 0x04;
+
+ private static final String CIPHER_ALG = "AES";
+ private static final String EC_ALG = "EC";
+ private static final String EC_P256_COMMON_NAME = "secp256r1";
+ private static final String EC_P256_OPENSSL_NAME = "prime256v1";
+ private static final String ENC_ALG = "AES/GCM/NoPadding";
+ private static final String KA_ALG = "ECDH";
+ private static final String MAC_ALG = "HmacSHA256";
+
+ private static final int EC_COORDINATE_LEN_BYTES = 32;
+ private static final int EC_PUBLIC_KEY_LEN_BYTES = 2 * EC_COORDINATE_LEN_BYTES + 1;
+ private static final int GCM_NONCE_LEN_BYTES = 12;
+ private static final int GCM_KEY_LEN_BYTES = 16;
+ private static final int GCM_TAG_LEN_BYTES = 16;
+
+ private static final BigInteger BIG_INT_02 = BigInteger.valueOf(2);
+
+ private enum AesGcmOperation {
+ ENCRYPT,
+ DECRYPT
+ }
+
+ // Parameters for the NIST P-256 curve y^2 = x^3 + ax + b (mod p)
+ private static final BigInteger EC_PARAM_P =
+ new BigInteger("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", 16);
+ private static final BigInteger EC_PARAM_A = EC_PARAM_P.subtract(new BigInteger("3"));
+ private static final BigInteger EC_PARAM_B =
+ new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16);
+
+ @VisibleForTesting static final ECParameterSpec EC_PARAM_SPEC;
+
+ static {
+ EllipticCurve curveSpec =
+ new EllipticCurve(new ECFieldFp(EC_PARAM_P), EC_PARAM_A, EC_PARAM_B);
+ ECPoint generator =
+ new ECPoint(
+ new BigInteger(
+ "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296",
+ 16),
+ new BigInteger(
+ "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5",
+ 16));
+ BigInteger generatorOrder =
+ new BigInteger(
+ "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16);
+ EC_PARAM_SPEC = new ECParameterSpec(curveSpec, generator, generatorOrder, /* cofactor */ 1);
+ }
+
+ private SecureBox() {}
+
+ /**
+ * Randomly generates a public-key pair that can be used for the functions {@link #encrypt} and
+ * {@link #decrypt}.
+ *
+ * @return the randomly generated public-key pair
+ * @throws NoSuchAlgorithmException if the underlying crypto algorithm is not supported
+ * @hide
+ */
+ public static KeyPair genKeyPair() throws NoSuchAlgorithmException {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(EC_ALG);
+ try {
+ // Try using the OpenSSL provider first
+ keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_OPENSSL_NAME));
+ return keyPairGenerator.generateKeyPair();
+ } catch (InvalidAlgorithmParameterException ex) {
+ // Try another name for NIST P-256
+ }
+ try {
+ keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_COMMON_NAME));
+ return keyPairGenerator.generateKeyPair();
+ } catch (InvalidAlgorithmParameterException ex) {
+ throw new NoSuchAlgorithmException("Unable to find the NIST P-256 curve", ex);
+ }
+ }
+
+ /**
+ * Encrypts {@code payload} by using {@code theirPublicKey} and/or {@code sharedSecret}. At
+ * least one of {@code theirPublicKey} and {@code sharedSecret} must be non-null, and an empty
+ * {@code sharedSecret} is equivalent to null.
+ *
+ * <p>Note that {@code header} will be authenticated (but not encrypted) together with {@code
+ * payload}, and the same {@code header} has to be provided for {@link #decrypt}.
+ *
+ * @param theirPublicKey the recipient's public key, or null if the payload is to be encrypted
+ * only with the shared secret
+ * @param sharedSecret the secret shared between the sender and the recipient, or null if the
+ * payload is to be encrypted only with the recipient's public key
+ * @param header the data that will be authenticated with {@code payload} but not encrypted, or
+ * null if the data is empty
+ * @param payload the data to be encrypted, or null if the data is empty
+ * @return the encrypted payload
+ * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported
+ * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms
+ * @hide
+ */
+ public static byte[] encrypt(
+ @Nullable PublicKey theirPublicKey,
+ @Nullable byte[] sharedSecret,
+ @Nullable byte[] header,
+ @Nullable byte[] payload)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ sharedSecret = emptyByteArrayIfNull(sharedSecret);
+ if (theirPublicKey == null && sharedSecret.length == 0) {
+ throw new IllegalArgumentException("Both the public key and shared secret are empty");
+ }
+ header = emptyByteArrayIfNull(header);
+ payload = emptyByteArrayIfNull(payload);
+
+ KeyPair senderKeyPair;
+ byte[] dhSecret;
+ byte[] hkdfInfo;
+ if (theirPublicKey == null) {
+ senderKeyPair = null;
+ dhSecret = EMPTY_BYTE_ARRAY;
+ hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY;
+ } else {
+ senderKeyPair = genKeyPair();
+ dhSecret = dhComputeSecret(senderKeyPair.getPrivate(), theirPublicKey);
+ hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY;
+ }
+
+ byte[] randNonce = genRandomNonce();
+ byte[] keyingMaterial = concat(dhSecret, sharedSecret);
+ SecretKey encryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo);
+ byte[] ciphertext = aesGcmEncrypt(encryptionKey, randNonce, payload, header);
+ if (senderKeyPair == null) {
+ return concat(VERSION, randNonce, ciphertext);
+ } else {
+ return concat(
+ VERSION, encodePublicKey(senderKeyPair.getPublic()), randNonce, ciphertext);
+ }
+ }
+
+ /**
+ * Decrypts {@code encryptedPayload} by using {@code ourPrivateKey} and/or {@code sharedSecret}.
+ * At least one of {@code ourPrivateKey} and {@code sharedSecret} must be non-null, and an empty
+ * {@code sharedSecret} is equivalent to null.
+ *
+ * <p>Note that {@code header} should be the same data used for {@link #encrypt}, which is
+ * authenticated (but not encrypted) together with {@code payload}; otherwise, an {@code
+ * AEADBadTagException} will be thrown.
+ *
+ * @param ourPrivateKey the recipient's private key, or null if the payload was encrypted only
+ * with the shared secret
+ * @param sharedSecret the secret shared between the sender and the recipient, or null if the
+ * payload was encrypted only with the recipient's public key
+ * @param header the data that was authenticated with the original payload but not encrypted, or
+ * null if the data is empty
+ * @param encryptedPayload the data to be decrypted
+ * @return the original payload that was encrypted
+ * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported
+ * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms
+ * @throws AEADBadTagException if the authentication tag contained in {@code encryptedPayload}
+ * cannot be validated, or if the payload is not a valid SecureBox V2 payload.
+ * @hide
+ */
+ public static byte[] decrypt(
+ @Nullable PrivateKey ourPrivateKey,
+ @Nullable byte[] sharedSecret,
+ @Nullable byte[] header,
+ byte[] encryptedPayload)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ sharedSecret = emptyByteArrayIfNull(sharedSecret);
+ if (ourPrivateKey == null && sharedSecret.length == 0) {
+ throw new IllegalArgumentException("Both the private key and shared secret are empty");
+ }
+ header = emptyByteArrayIfNull(header);
+ if (encryptedPayload == null) {
+ throw new NullPointerException("Encrypted payload must not be null.");
+ }
+
+ ByteBuffer ciphertextBuffer = ByteBuffer.wrap(encryptedPayload);
+ byte[] version = readEncryptedPayload(ciphertextBuffer, VERSION.length);
+ if (!Arrays.equals(version, VERSION)) {
+ throw new AEADBadTagException("The payload was not encrypted by SecureBox v2");
+ }
+
+ byte[] senderPublicKeyBytes;
+ byte[] dhSecret;
+ byte[] hkdfInfo;
+ if (ourPrivateKey == null) {
+ dhSecret = EMPTY_BYTE_ARRAY;
+ hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY;
+ } else {
+ senderPublicKeyBytes = readEncryptedPayload(ciphertextBuffer, EC_PUBLIC_KEY_LEN_BYTES);
+ dhSecret = dhComputeSecret(ourPrivateKey, decodePublicKey(senderPublicKeyBytes));
+ hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY;
+ }
+
+ byte[] randNonce = readEncryptedPayload(ciphertextBuffer, GCM_NONCE_LEN_BYTES);
+ byte[] ciphertext = readEncryptedPayload(ciphertextBuffer, ciphertextBuffer.remaining());
+ byte[] keyingMaterial = concat(dhSecret, sharedSecret);
+ SecretKey decryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo);
+ return aesGcmDecrypt(decryptionKey, randNonce, ciphertext, header);
+ }
+
+ private static byte[] readEncryptedPayload(ByteBuffer buffer, int length)
+ throws AEADBadTagException {
+ byte[] output = new byte[length];
+ try {
+ buffer.get(output);
+ } catch (BufferUnderflowException ex) {
+ throw new AEADBadTagException("The encrypted payload is too short");
+ }
+ return output;
+ }
+
+ private static byte[] dhComputeSecret(PrivateKey ourPrivateKey, PublicKey theirPublicKey)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ KeyAgreement agreement = KeyAgreement.getInstance(KA_ALG);
+ try {
+ agreement.init(ourPrivateKey);
+ } catch (RuntimeException ex) {
+ // Rethrow the RuntimeException as InvalidKeyException
+ throw new InvalidKeyException(ex);
+ }
+ agreement.doPhase(theirPublicKey, /*lastPhase=*/ true);
+ return agreement.generateSecret();
+ }
+
+ /** Derives a 128-bit AES key. */
+ private static SecretKey hkdfDeriveKey(byte[] secret, byte[] salt, byte[] info)
+ throws NoSuchAlgorithmException {
+ Mac mac = Mac.getInstance(MAC_ALG);
+ try {
+ mac.init(new SecretKeySpec(salt, MAC_ALG));
+ } catch (InvalidKeyException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ byte[] pseudorandomKey = mac.doFinal(secret);
+
+ try {
+ mac.init(new SecretKeySpec(pseudorandomKey, MAC_ALG));
+ } catch (InvalidKeyException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ mac.update(info);
+ // Hashing just one block will yield 256 bits, which is enough to construct the AES key
+ byte[] hkdfOutput = mac.doFinal(CONSTANT_01);
+
+ return new SecretKeySpec(Arrays.copyOf(hkdfOutput, GCM_KEY_LEN_BYTES), CIPHER_ALG);
+ }
+
+ private static byte[] aesGcmEncrypt(SecretKey key, byte[] nonce, byte[] plaintext, byte[] aad)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ try {
+ return aesGcmInternal(AesGcmOperation.ENCRYPT, key, nonce, plaintext, aad);
+ } catch (AEADBadTagException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private static byte[] aesGcmDecrypt(SecretKey key, byte[] nonce, byte[] ciphertext, byte[] aad)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ return aesGcmInternal(AesGcmOperation.DECRYPT, key, nonce, ciphertext, aad);
+ }
+
+ private static byte[] aesGcmInternal(
+ AesGcmOperation operation, SecretKey key, byte[] nonce, byte[] text, byte[] aad)
+ throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(ENC_ALG);
+ } catch (NoSuchPaddingException ex) {
+ // This should never happen because AES-GCM doesn't use padding
+ throw new RuntimeException(ex);
+ }
+ GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN_BYTES * 8, nonce);
+ try {
+ if (operation == AesGcmOperation.DECRYPT) {
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+ } else {
+ cipher.init(Cipher.ENCRYPT_MODE, key, spec);
+ }
+ } catch (InvalidAlgorithmParameterException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ try {
+ cipher.updateAAD(aad);
+ return cipher.doFinal(text);
+ } catch (AEADBadTagException ex) {
+ // Catch and rethrow AEADBadTagException first because it's a subclass of
+ // BadPaddingException
+ throw ex;
+ } catch (IllegalBlockSizeException | BadPaddingException ex) {
+ // This should never happen because AES-GCM can handle inputs of any length without
+ // padding
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Encodes public key in format expected by the secure hardware module. This is used as part
+ * of the vault params.
+ *
+ * @param publicKey The public key.
+ * @return The key packed into a 65-byte array.
+ */
+ static byte[] encodePublicKey(PublicKey publicKey) {
+ ECPoint point = ((ECPublicKey) publicKey).getW();
+ byte[] x = point.getAffineX().toByteArray();
+ byte[] y = point.getAffineY().toByteArray();
+
+ byte[] output = new byte[EC_PUBLIC_KEY_LEN_BYTES];
+ // The order of arraycopy() is important, because the coordinates may have a one-byte
+ // leading 0 for the sign bit of two's complement form
+ System.arraycopy(y, 0, output, EC_PUBLIC_KEY_LEN_BYTES - y.length, y.length);
+ System.arraycopy(x, 0, output, 1 + EC_COORDINATE_LEN_BYTES - x.length, x.length);
+ output[0] = EC_PUBLIC_KEY_PREFIX;
+ return output;
+ }
+
+ @VisibleForTesting
+ static PublicKey decodePublicKey(byte[] keyBytes)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ BigInteger x =
+ new BigInteger(
+ /*signum=*/ 1,
+ Arrays.copyOfRange(keyBytes, 1, 1 + EC_COORDINATE_LEN_BYTES));
+ BigInteger y =
+ new BigInteger(
+ /*signum=*/ 1,
+ Arrays.copyOfRange(
+ keyBytes, 1 + EC_COORDINATE_LEN_BYTES, EC_PUBLIC_KEY_LEN_BYTES));
+
+ // Checks if the point is indeed on the P-256 curve for security considerations
+ validateEcPoint(x, y);
+
+ KeyFactory keyFactory = KeyFactory.getInstance(EC_ALG);
+ try {
+ return keyFactory.generatePublic(new ECPublicKeySpec(new ECPoint(x, y), EC_PARAM_SPEC));
+ } catch (InvalidKeySpecException ex) {
+ // This should never happen
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private static void validateEcPoint(BigInteger x, BigInteger y) throws InvalidKeyException {
+ if (x.compareTo(EC_PARAM_P) >= 0
+ || y.compareTo(EC_PARAM_P) >= 0
+ || x.signum() == -1
+ || y.signum() == -1) {
+ throw new InvalidKeyException("Point lies outside of the expected curve");
+ }
+
+ // Points on the curve satisfy y^2 = x^3 + ax + b (mod p)
+ BigInteger lhs = y.modPow(BIG_INT_02, EC_PARAM_P);
+ BigInteger rhs =
+ x.modPow(BIG_INT_02, EC_PARAM_P) // x^2
+ .add(EC_PARAM_A) // x^2 + a
+ .mod(EC_PARAM_P) // This will speed up the next multiplication
+ .multiply(x) // (x^2 + a) * x = x^3 + ax
+ .add(EC_PARAM_B) // x^3 + ax + b
+ .mod(EC_PARAM_P);
+ if (!lhs.equals(rhs)) {
+ throw new InvalidKeyException("Point lies outside of the expected curve");
+ }
+ }
+
+ private static byte[] genRandomNonce() throws NoSuchAlgorithmException {
+ byte[] nonce = new byte[GCM_NONCE_LEN_BYTES];
+ new SecureRandom().nextBytes(nonce);
+ return nonce;
+ }
+
+ @VisibleForTesting
+ static byte[] concat(byte[]... inputs) {
+ int length = 0;
+ for (int i = 0; i < inputs.length; i++) {
+ if (inputs[i] == null) {
+ inputs[i] = EMPTY_BYTE_ARRAY;
+ }
+ length += inputs[i].length;
+ }
+
+ byte[] output = new byte[length];
+ int outputPos = 0;
+ for (byte[] input : inputs) {
+ System.arraycopy(input, /*srcPos=*/ 0, output, outputPos, input.length);
+ outputPos += input.length;
+ }
+ return output;
+ }
+
+ private static byte[] emptyByteArrayIfNull(@Nullable byte[] input) {
+ return input == null ? EMPTY_BYTE_ARRAY : input;
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/WrappedKey.java b/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
new file mode 100644
index 00000000..54aa9f08
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/WrappedKey.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 com.android.server.locksettings.recoverablekeystore;
+
+import android.util.Log;
+import android.security.recoverablekeystore.RecoverableKeyStoreLoader;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+/**
+ * A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding.
+ *
+ * @hide
+ */
+public class WrappedKey {
+ private static final String TAG = "WrappedKey";
+
+ private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+ private static final String APPLICATION_KEY_ALGORITHM = "AES";
+ private static final int GCM_TAG_LENGTH_BITS = 128;
+
+ private final int mPlatformKeyGenerationId;
+ private final int mRecoveryStatus;
+ private final byte[] mNonce;
+ private final byte[] mKeyMaterial;
+
+ /**
+ * Returns a wrapped form of {@code key}, using {@code wrappingKey} to encrypt the key material.
+ *
+ * @throws InvalidKeyException if {@code wrappingKey} cannot be used to encrypt {@code key}, or
+ * if {@code key} does not expose its key material. See
+ * {@link android.security.keystore.AndroidKeyStoreKey} for an example of a key that does
+ * not expose its key material.
+ */
+ public static WrappedKey fromSecretKey(PlatformEncryptionKey wrappingKey, SecretKey key)
+ throws InvalidKeyException, KeyStoreException {
+ if (key.getEncoded() == null) {
+ throw new InvalidKeyException(
+ "key does not expose encoded material. It cannot be wrapped.");
+ }
+
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new RuntimeException(
+ "Android does not support AES/GCM/NoPadding. This should never happen.");
+ }
+
+ cipher.init(Cipher.WRAP_MODE, wrappingKey.getKey());
+ byte[] encryptedKeyMaterial;
+ try {
+ encryptedKeyMaterial = cipher.wrap(key);
+ } catch (IllegalBlockSizeException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof KeyStoreException) {
+ // If AndroidKeyStore encounters any error here, it throws IllegalBlockSizeException
+ // with KeyStoreException as the cause. This is due to there being no better option
+ // here, as the Cipher#wrap only checked throws InvalidKeyException or
+ // IllegalBlockSizeException. If this is the case, we want to propagate it to the
+ // caller, so rethrow the cause.
+ throw (KeyStoreException) cause;
+ } else {
+ throw new RuntimeException(
+ "IllegalBlockSizeException should not be thrown by AES/GCM/NoPadding mode.",
+ e);
+ }
+ }
+
+ return new WrappedKey(
+ /*nonce=*/ cipher.getIV(),
+ /*keyMaterial=*/ encryptedKeyMaterial,
+ /*platformKeyGenerationId=*/ wrappingKey.getGenerationId(),
+ RecoverableKeyStoreLoader.RECOVERY_STATUS_SYNC_IN_PROGRESS);
+ }
+
+ /**
+ * A new instance with default recovery status.
+ *
+ * @param nonce The nonce with which the key material was encrypted.
+ * @param keyMaterial The encrypted bytes of the key material.
+ * @param platformKeyGenerationId The generation ID of the key used to wrap this key.
+ *
+ * @see RecoverableKeyStoreLoader.RECOVERY_STATUS_SYNC_IN_PROGRESS
+ * @hide
+ */
+ public WrappedKey(byte[] nonce, byte[] keyMaterial, int platformKeyGenerationId) {
+ mNonce = nonce;
+ mKeyMaterial = keyMaterial;
+ mPlatformKeyGenerationId = platformKeyGenerationId;
+ mRecoveryStatus = RecoverableKeyStoreLoader.RECOVERY_STATUS_SYNC_IN_PROGRESS;
+ }
+
+ /**
+ * A new instance.
+ *
+ * @param nonce The nonce with which the key material was encrypted.
+ * @param keyMaterial The encrypted bytes of the key material.
+ * @param platformKeyGenerationId The generation ID of the key used to wrap this key.
+ * @param recoveryStatus recovery status of the key.
+ *
+ * @hide
+ */
+ public WrappedKey(byte[] nonce, byte[] keyMaterial, int platformKeyGenerationId,
+ int recoveryStatus) {
+ mNonce = nonce;
+ mKeyMaterial = keyMaterial;
+ mPlatformKeyGenerationId = platformKeyGenerationId;
+ mRecoveryStatus = recoveryStatus;
+ }
+
+ /**
+ * Returns the nonce with which the key material was encrypted.
+ *
+ * @hide
+ */
+ public byte[] getNonce() {
+ return mNonce;
+ }
+
+ /**
+ * Returns the encrypted key material.
+ *
+ * @hide
+ */
+ public byte[] getKeyMaterial() {
+ return mKeyMaterial;
+ }
+
+ /**
+ * Returns the generation ID of the platform key, with which this key was wrapped.
+ *
+ * @hide
+ */
+ public int getPlatformKeyGenerationId() {
+ return mPlatformKeyGenerationId;
+ }
+
+ /**
+ * Returns recovery status of the key.
+ *
+ * @hide
+ */
+ public int getRecoveryStatus() {
+ return mRecoveryStatus;
+ }
+
+ /**
+ * Unwraps the {@code wrappedKeys} with the {@code platformKey}.
+ *
+ * @return The unwrapped keys, indexed by alias.
+ * @throws NoSuchAlgorithmException if AES/GCM/NoPadding Cipher or AES key type is unavailable.
+ * @throws BadPlatformKeyException if the {@code platformKey} has a different generation ID to
+ * any of the {@code wrappedKeys}.
+ *
+ * @hide
+ */
+ public static Map<String, SecretKey> unwrapKeys(
+ PlatformDecryptionKey platformKey,
+ Map<String, WrappedKey> wrappedKeys)
+ throws NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException {
+ HashMap<String, SecretKey> unwrappedKeys = new HashMap<>();
+ Cipher cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
+ int platformKeyGenerationId = platformKey.getGenerationId();
+
+ for (String alias : wrappedKeys.keySet()) {
+ WrappedKey wrappedKey = wrappedKeys.get(alias);
+ if (wrappedKey.getPlatformKeyGenerationId() != platformKeyGenerationId) {
+ throw new BadPlatformKeyException(String.format(
+ Locale.US,
+ "WrappedKey with alias '%s' was wrapped with platform key %d, not "
+ + "platform key %d",
+ alias,
+ wrappedKey.getPlatformKeyGenerationId(),
+ platformKey.getGenerationId()));
+ }
+
+ try {
+ cipher.init(
+ Cipher.UNWRAP_MODE,
+ platformKey.getKey(),
+ new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG,
+ String.format(
+ Locale.US,
+ "Could not init Cipher to unwrap recoverable key with alias '%s'",
+ alias),
+ e);
+ continue;
+ }
+ SecretKey key;
+ try {
+ key = (SecretKey) cipher.unwrap(
+ wrappedKey.getKeyMaterial(), APPLICATION_KEY_ALGORITHM, Cipher.SECRET_KEY);
+ } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+ Log.e(TAG,
+ String.format(
+ Locale.US,
+ "Error unwrapping recoverable key with alias '%s'",
+ alias),
+ e);
+ continue;
+ }
+ unwrappedKeys.put(alias, key);
+ }
+
+ return unwrappedKeys;
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
new file mode 100644
index 00000000..838311e1
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.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 com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.server.locksettings.recoverablekeystore.WrappedKey;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.RecoveryServiceMetadataEntry;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.UserMetadataEntry;
+
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringJoiner;
+
+/**
+ * Database of recoverable key information.
+ *
+ * @hide
+ */
+public class RecoverableKeyStoreDb {
+ private static final String TAG = "RecoverableKeyStoreDb";
+ private static final int IDLE_TIMEOUT_SECONDS = 30;
+ private static final int LAST_SYNCED_AT_UNSYNCED = -1;
+
+ private final RecoverableKeyStoreDbHelper mKeyStoreDbHelper;
+
+ /**
+ * A new instance, storing the database in the user directory of {@code context}.
+ *
+ * @hide
+ */
+ public static RecoverableKeyStoreDb newInstance(Context context) {
+ RecoverableKeyStoreDbHelper helper = new RecoverableKeyStoreDbHelper(context);
+ helper.setWriteAheadLoggingEnabled(true);
+ helper.setIdleConnectionTimeout(IDLE_TIMEOUT_SECONDS);
+ return new RecoverableKeyStoreDb(helper);
+ }
+
+ private RecoverableKeyStoreDb(RecoverableKeyStoreDbHelper keyStoreDbHelper) {
+ this.mKeyStoreDbHelper = keyStoreDbHelper;
+ }
+
+ /**
+ * Inserts a key into the database.
+ *
+ * @param userId The uid of the profile the application is running under.
+ * @param uid Uid of the application to whom the key belongs.
+ * @param alias The alias of the key in the AndroidKeyStore.
+ * @param wrappedKey The wrapped key.
+ * @return The primary key of the inserted row, or -1 if failed.
+ *
+ * @hide
+ */
+ public long insertKey(int userId, int uid, String alias, WrappedKey wrappedKey) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(KeysEntry.COLUMN_NAME_USER_ID, userId);
+ values.put(KeysEntry.COLUMN_NAME_UID, uid);
+ values.put(KeysEntry.COLUMN_NAME_ALIAS, alias);
+ values.put(KeysEntry.COLUMN_NAME_NONCE, wrappedKey.getNonce());
+ values.put(KeysEntry.COLUMN_NAME_WRAPPED_KEY, wrappedKey.getKeyMaterial());
+ values.put(KeysEntry.COLUMN_NAME_LAST_SYNCED_AT, LAST_SYNCED_AT_UNSYNCED);
+ values.put(KeysEntry.COLUMN_NAME_GENERATION_ID, wrappedKey.getPlatformKeyGenerationId());
+ values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS, wrappedKey.getRecoveryStatus());
+ return db.replace(KeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
+ }
+
+ /**
+ * Gets the key with {@code alias} for the app with {@code uid}.
+ *
+ * @hide
+ */
+ @Nullable public WrappedKey getKey(int uid, String alias) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+ String[] projection = {
+ KeysEntry._ID,
+ KeysEntry.COLUMN_NAME_NONCE,
+ KeysEntry.COLUMN_NAME_WRAPPED_KEY,
+ KeysEntry.COLUMN_NAME_GENERATION_ID,
+ KeysEntry.COLUMN_NAME_RECOVERY_STATUS};
+ String selection =
+ KeysEntry.COLUMN_NAME_UID + " = ? AND "
+ + KeysEntry.COLUMN_NAME_ALIAS + " = ?";
+ String[] selectionArguments = { Integer.toString(uid), alias };
+
+ try (
+ Cursor cursor = db.query(
+ KeysEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ int count = cursor.getCount();
+ if (count == 0) {
+ return null;
+ }
+ if (count > 1) {
+ Log.wtf(TAG,
+ String.format(Locale.US,
+ "%d WrappedKey entries found for uid=%d alias='%s'. "
+ + "Should only ever be 0 or 1.", count, uid, alias));
+ return null;
+ }
+ cursor.moveToFirst();
+ byte[] nonce = cursor.getBlob(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
+ byte[] keyMaterial = cursor.getBlob(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
+ int generationId = cursor.getInt(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_GENERATION_ID));
+ int recoveryStatus = cursor.getInt(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_RECOVERY_STATUS));
+ return new WrappedKey(nonce, keyMaterial, generationId, recoveryStatus);
+ }
+ }
+
+ /**
+ * Returns all statuses for keys {@code uid} and {@code platformKeyGenerationId}.
+ *
+ * @param uid of the application
+ *
+ * @return Map from Aliases to status.
+ *
+ * @hide
+ */
+ public @NonNull Map<String, Integer> getStatusForAllKeys(int uid) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+ String[] projection = {
+ KeysEntry._ID,
+ KeysEntry.COLUMN_NAME_ALIAS,
+ KeysEntry.COLUMN_NAME_RECOVERY_STATUS};
+ String selection =
+ KeysEntry.COLUMN_NAME_UID + " = ?";
+ String[] selectionArguments = {Integer.toString(uid)};
+
+ try (
+ Cursor cursor = db.query(
+ KeysEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ HashMap<String, Integer> statuses = new HashMap<>();
+ while (cursor.moveToNext()) {
+ String alias = cursor.getString(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_ALIAS));
+ int recoveryStatus = cursor.getInt(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_RECOVERY_STATUS));
+ statuses.put(alias, recoveryStatus);
+ }
+ return statuses;
+ }
+ }
+
+ /**
+ * Updates status for given key.
+ * @param uid of the application
+ * @param alias of the key
+ * @param status - new status
+ * @return number of updated entries.
+ * @hide
+ **/
+ public int setRecoveryStatus(int uid, String alias, int status) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(KeysEntry.COLUMN_NAME_RECOVERY_STATUS, status);
+ String selection =
+ KeysEntry.COLUMN_NAME_UID + " = ? AND "
+ + KeysEntry.COLUMN_NAME_ALIAS + " = ?";
+ return db.update(KeysEntry.TABLE_NAME, values, selection,
+ new String[] {String.valueOf(uid), alias});
+ }
+
+ /**
+ * Returns all keys for the given {@code userId} and {@code platformKeyGenerationId}.
+ *
+ * @param userId User id of the profile to which all the keys are associated.
+ * @param platformKeyGenerationId The generation ID of the platform key that wrapped these keys.
+ * (i.e., this should be the most recent generation ID, as older platform keys are not
+ * usable.)
+ *
+ * @hide
+ */
+ public Map<String, WrappedKey> getAllKeys(int userId, int platformKeyGenerationId) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+ String[] projection = {
+ KeysEntry._ID,
+ KeysEntry.COLUMN_NAME_NONCE,
+ KeysEntry.COLUMN_NAME_WRAPPED_KEY,
+ KeysEntry.COLUMN_NAME_ALIAS,
+ KeysEntry.COLUMN_NAME_RECOVERY_STATUS};
+ String selection =
+ KeysEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ + KeysEntry.COLUMN_NAME_GENERATION_ID + " = ?";
+ String[] selectionArguments = {
+ Integer.toString(userId), Integer.toString(platformKeyGenerationId) };
+
+ try (
+ Cursor cursor = db.query(
+ KeysEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ HashMap<String, WrappedKey> keys = new HashMap<>();
+ while (cursor.moveToNext()) {
+ byte[] nonce = cursor.getBlob(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
+ byte[] keyMaterial = cursor.getBlob(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
+ String alias = cursor.getString(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_ALIAS));
+ int recoveryStatus = cursor.getInt(
+ cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_RECOVERY_STATUS));
+ keys.put(alias, new WrappedKey(nonce, keyMaterial, platformKeyGenerationId,
+ recoveryStatus));
+ }
+ return keys;
+ }
+ }
+
+ /**
+ * Sets the {@code generationId} of the platform key for the account owned by {@code userId}.
+ *
+ * @return The primary key ID of the relation.
+ */
+ public long setPlatformKeyGenerationId(int userId, int generationId) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, userId);
+ values.put(UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID, generationId);
+ return db.replace(
+ UserMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
+ }
+
+ /**
+ * Returns the generation ID associated with the platform key of the user with {@code userId}.
+ */
+ public int getPlatformKeyGenerationId(int userId) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+ String[] projection = {
+ UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID};
+ String selection =
+ UserMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
+ String[] selectionArguments = {
+ Integer.toString(userId)};
+
+ try (
+ Cursor cursor = db.query(
+ UserMetadataEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ if (cursor.getCount() == 0) {
+ return -1;
+ }
+ cursor.moveToFirst();
+ return cursor.getInt(
+ cursor.getColumnIndexOrThrow(
+ UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID));
+ }
+ }
+
+ /**
+ * Updates the public key of the recovery service into the database.
+ *
+ * @param userId The uid of the profile the application is running under.
+ * @param uid The uid of the application to whom the key belongs.
+ * @param publicKey The public key of the recovery service.
+ * @return The primary key of the inserted row, or -1 if failed.
+ *
+ * @hide
+ */
+ public long setRecoveryServicePublicKey(int userId, int uid, PublicKey publicKey) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY, publicKey.getEncoded());
+ String selection =
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
+ String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
+
+ ensureRecoveryServiceMetadataEntryExists(userId, uid);
+ return db.update(
+ RecoveryServiceMetadataEntry.TABLE_NAME, values, selection, selectionArguments);
+ }
+
+ /**
+ * Returns the uid of the recovery agent for the given user, or -1 if none is set.
+ */
+ public int getRecoveryAgentUid(int userId) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+
+ String[] projection = { RecoveryServiceMetadataEntry.COLUMN_NAME_UID };
+ String selection = RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
+ String[] selectionArguments = { Integer.toString(userId) };
+
+ try (
+ Cursor cursor = db.query(
+ RecoveryServiceMetadataEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ int count = cursor.getCount();
+ if (count == 0) {
+ return -1;
+ }
+ cursor.moveToFirst();
+ return cursor.getInt(
+ cursor.getColumnIndexOrThrow(RecoveryServiceMetadataEntry.COLUMN_NAME_UID));
+ }
+ }
+
+ /**
+ * Returns the public key of the recovery service.
+ *
+ * @param userId The uid of the profile the application is running under.
+ * @param uid The uid of the application who initializes the local recovery components.
+ *
+ * @hide
+ */
+ @Nullable
+ public PublicKey getRecoveryServicePublicKey(int userId, int uid) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+
+ String[] projection = {
+ RecoveryServiceMetadataEntry._ID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY};
+ String selection =
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
+ String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
+
+ try (
+ Cursor cursor = db.query(
+ RecoveryServiceMetadataEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ int count = cursor.getCount();
+ if (count == 0) {
+ return null;
+ }
+ if (count > 1) {
+ Log.wtf(TAG,
+ String.format(Locale.US,
+ "%d PublicKey entries found for userId=%d uid=%d. "
+ + "Should only ever be 0 or 1.", count, userId, uid));
+ return null;
+ }
+ cursor.moveToFirst();
+ int idx = cursor.getColumnIndexOrThrow(
+ RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY);
+ if (cursor.isNull(idx)) {
+ return null;
+ }
+ byte[] keyBytes = cursor.getBlob(idx);
+ try {
+ return decodeX509Key(keyBytes);
+ } catch (InvalidKeySpecException e) {
+ Log.wtf(TAG,
+ String.format(Locale.US,
+ "Recovery service public key entry cannot be decoded for "
+ + "userId=%d uid=%d.",
+ userId, uid));
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Updates the list of user secret types used for end-to-end encryption.
+ * If no secret types are set, recovery snapshot will not be created.
+ * See {@code KeyStoreRecoveryMetadata}
+ *
+ * @param userId The uid of the profile the application is running under.
+ * @param uid The uid of the application.
+ * @param secretTypes list of secret types
+ * @return The primary key of the updated row, or -1 if failed.
+ *
+ * @hide
+ */
+ public long setRecoverySecretTypes(int userId, int uid, int[] secretTypes) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ StringJoiner joiner = new StringJoiner(",");
+ Arrays.stream(secretTypes).forEach(i -> joiner.add(Integer.toString(i)));
+ String typesAsCsv = joiner.toString();
+ values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES, typesAsCsv);
+ String selection =
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
+ ensureRecoveryServiceMetadataEntryExists(userId, uid);
+ return db.update(RecoveryServiceMetadataEntry.TABLE_NAME, values, selection,
+ new String[] {String.valueOf(userId), String.valueOf(uid)});
+ }
+
+ /**
+ * Returns the list of secret types used for end-to-end encryption.
+ *
+ * @param userId The uid of the profile the application is running under.
+ * @param uid The uid of the application who initialized the local recovery components.
+ * @return Secret types or empty array, if types were not set.
+ *
+ * @hide
+ */
+ public @NonNull int[] getRecoverySecretTypes(int userId, int uid) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+
+ String[] projection = {
+ RecoveryServiceMetadataEntry._ID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES};
+ String selection =
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
+ String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
+
+ try (
+ Cursor cursor = db.query(
+ RecoveryServiceMetadataEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ int count = cursor.getCount();
+ if (count == 0) {
+ return new int[]{};
+ }
+ if (count > 1) {
+ Log.wtf(TAG,
+ String.format(Locale.US,
+ "%d deviceId entries found for userId=%d uid=%d. "
+ + "Should only ever be 0 or 1.", count, userId, uid));
+ return new int[]{};
+ }
+ cursor.moveToFirst();
+ int idx = cursor.getColumnIndexOrThrow(
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES);
+ if (cursor.isNull(idx)) {
+ return new int[]{};
+ }
+ String csv = cursor.getString(idx);
+ if (TextUtils.isEmpty(csv)) {
+ return new int[]{};
+ }
+ String[] types = csv.split(",");
+ int[] result = new int[types.length];
+ for (int i = 0; i < types.length; i++) {
+ try {
+ result[i] = Integer.parseInt(types[i]);
+ } catch (NumberFormatException e) {
+ Log.wtf(TAG, "String format error " + e);
+ }
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Returns the first (and only?) public key for {@code userId}.
+ *
+ * @param userId The uid of the profile whose keys are to be synced.
+ * @return The public key, or null if none exists.
+ */
+ @Nullable
+ public PublicKey getRecoveryServicePublicKey(int userId) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+
+ String[] projection = { RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY };
+ String selection =
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
+ String[] selectionArguments = { Integer.toString(userId) };
+
+ try (
+ Cursor cursor = db.query(
+ RecoveryServiceMetadataEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ if (cursor.getCount() < 1) {
+ return null;
+ }
+
+ cursor.moveToFirst();
+ byte[] keyBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(
+ RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY));
+
+ try {
+ return decodeX509Key(keyBytes);
+ } catch (InvalidKeySpecException e) {
+ Log.wtf(TAG, "Could not decode public key for " + userId);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Updates the server parameters given by the application initializing the local recovery
+ * components.
+ *
+ * @param userId The uid of the profile the application is running under.
+ * @param uid The uid of the application.
+ * @param serverParameters The server parameters.
+ * @return The primary key of the inserted row, or -1 if failed.
+ *
+ * @hide
+ */
+ public long setServerParameters(int userId, int uid, long serverParameters) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMETERS, serverParameters);
+ String selection =
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
+ String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
+
+ ensureRecoveryServiceMetadataEntryExists(userId, uid);
+ return db.update(
+ RecoveryServiceMetadataEntry.TABLE_NAME, values, selection, selectionArguments);
+ }
+
+ /**
+ * Returns the server paramters that was previously set by the application who initialized the
+ * local recovery service components.
+ *
+ * @param userId The uid of the profile the application is running under.
+ * @param uid The uid of the application who initialized the local recovery components.
+ * @return The server parameters that were previously set, or null if there's none.
+ *
+ * @hide
+ */
+ public Long getServerParameters(int userId, int uid) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+
+ String[] projection = {
+ RecoveryServiceMetadataEntry._ID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMETERS};
+ String selection =
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
+ String[] selectionArguments = {Integer.toString(userId), Integer.toString(uid)};
+
+ try (
+ Cursor cursor = db.query(
+ RecoveryServiceMetadataEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ int count = cursor.getCount();
+ if (count == 0) {
+ return null;
+ }
+ if (count > 1) {
+ Log.wtf(TAG,
+ String.format(Locale.US,
+ "%d deviceId entries found for userId=%d uid=%d. "
+ + "Should only ever be 0 or 1.", count, userId, uid));
+ return null;
+ }
+ cursor.moveToFirst();
+ int idx = cursor.getColumnIndexOrThrow(
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMETERS);
+ if (cursor.isNull(idx)) {
+ return null;
+ } else {
+ return cursor.getLong(idx);
+ }
+ }
+ }
+
+ /**
+ * Creates an empty row in the recovery service metadata table if such a row doesn't exist for
+ * the given userId and uid, so db.update will succeed.
+ */
+ private void ensureRecoveryServiceMetadataEntryExists(int userId, int uid) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID, userId);
+ values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_UID, uid);
+ db.insertWithOnConflict(RecoveryServiceMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null,
+ values, SQLiteDatabase.CONFLICT_IGNORE);
+ }
+
+ /**
+ * Closes all open connections to the database.
+ */
+ public void close() {
+ mKeyStoreDbHelper.close();
+ }
+
+ @Nullable
+ private static PublicKey decodeX509Key(byte[] keyBytes) throws InvalidKeySpecException {
+ X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(keyBytes);
+ try {
+ return KeyFactory.getInstance("EC").generatePublic(publicKeySpec);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
new file mode 100644
index 00000000..8f773ddd
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.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 com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.provider.BaseColumns;
+
+/**
+ * Contract for recoverable key database. Describes the tables present.
+ */
+class RecoverableKeyStoreDbContract {
+ /**
+ * Table holding wrapped keys, and information about when they were last synced.
+ */
+ static class KeysEntry implements BaseColumns {
+ static final String TABLE_NAME = "keys";
+
+ /**
+ * The user id of the profile the application is running under.
+ */
+ static final String COLUMN_NAME_USER_ID = "user_id";
+
+ /**
+ * The uid of the application that generated the key.
+ */
+ static final String COLUMN_NAME_UID = "uid";
+
+ /**
+ * The alias of the key, as set in AndroidKeyStore.
+ */
+ static final String COLUMN_NAME_ALIAS = "alias";
+
+ /**
+ * Nonce with which the key was encrypted.
+ */
+ static final String COLUMN_NAME_NONCE = "nonce";
+
+ /**
+ * Encrypted bytes of the key.
+ */
+ static final String COLUMN_NAME_WRAPPED_KEY = "wrapped_key";
+
+ /**
+ * Generation ID of the platform key that was used to encrypt this key.
+ */
+ static final String COLUMN_NAME_GENERATION_ID = "platform_key_generation_id";
+
+ /**
+ * Timestamp of when this key was last synced with remote storage, or -1 if never synced.
+ */
+ static final String COLUMN_NAME_LAST_SYNCED_AT = "last_synced_at";
+
+ /**
+ * Status of the key sync {@code RecoverableKeyStoreLoader#setRecoveryStatus}
+ */
+ static final String COLUMN_NAME_RECOVERY_STATUS = "recovery_status";
+ }
+
+ /**
+ * Recoverable KeyStore metadata for a specific user profile.
+ */
+ static class UserMetadataEntry implements BaseColumns {
+ static final String TABLE_NAME = "user_metadata";
+
+ /**
+ * User ID of the profile.
+ */
+ static final String COLUMN_NAME_USER_ID = "user_id";
+
+ /**
+ * Every time a new platform key is generated for a user, this increments. The platform key
+ * is used to wrap recoverable keys on disk.
+ */
+ static final String COLUMN_NAME_PLATFORM_KEY_GENERATION_ID = "platform_key_generation_id";
+ }
+
+ /**
+ * Table holding metadata of the recovery service.
+ */
+ static class RecoveryServiceMetadataEntry implements BaseColumns {
+ static final String TABLE_NAME = "recovery_service_metadata";
+
+ /**
+ * The user id of the profile the application is running under.
+ */
+ static final String COLUMN_NAME_USER_ID = "user_id";
+
+ /**
+ * The uid of the application that initializes the local recovery components.
+ */
+ static final String COLUMN_NAME_UID = "uid";
+
+ /**
+ * The public key of the recovery service.
+ */
+ static final String COLUMN_NAME_PUBLIC_KEY = "public_key";
+
+ /**
+ * Secret types used for end-to-end encryption.
+ */
+ static final String COLUMN_NAME_SECRET_TYPES = "secret_types";
+
+ /**
+ * The server parameters of the recovery service.
+ */
+ static final String COLUMN_NAME_SERVER_PARAMETERS = "server_parameters";
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
new file mode 100644
index 00000000..5b07f3e2
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
@@ -0,0 +1,93 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.RecoveryServiceMetadataEntry;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.UserMetadataEntry;
+
+/**
+ * Helper for creating the recoverable key database.
+ */
+class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
+ private static final int DATABASE_VERSION = 1;
+ private static final String DATABASE_NAME = "recoverablekeystore.db";
+
+ private static final String SQL_CREATE_KEYS_ENTRY =
+ "CREATE TABLE " + KeysEntry.TABLE_NAME + "( "
+ + KeysEntry._ID + " INTEGER PRIMARY KEY,"
+ + KeysEntry.COLUMN_NAME_USER_ID + " INTEGER,"
+ + KeysEntry.COLUMN_NAME_UID + " INTEGER,"
+ + KeysEntry.COLUMN_NAME_ALIAS + " TEXT,"
+ + KeysEntry.COLUMN_NAME_NONCE + " BLOB,"
+ + KeysEntry.COLUMN_NAME_WRAPPED_KEY + " BLOB,"
+ + KeysEntry.COLUMN_NAME_GENERATION_ID + " INTEGER,"
+ + KeysEntry.COLUMN_NAME_LAST_SYNCED_AT + " INTEGER,"
+ + KeysEntry.COLUMN_NAME_RECOVERY_STATUS + " INTEGER,"
+ + "UNIQUE(" + KeysEntry.COLUMN_NAME_UID + ","
+ + KeysEntry.COLUMN_NAME_ALIAS + "))";
+
+ private static final String SQL_CREATE_USER_METADATA_ENTRY =
+ "CREATE TABLE " + UserMetadataEntry.TABLE_NAME + "( "
+ + UserMetadataEntry._ID + " INTEGER PRIMARY KEY,"
+ + UserMetadataEntry.COLUMN_NAME_USER_ID + " INTEGER UNIQUE,"
+ + UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID + " INTEGER)";
+
+ private static final String SQL_CREATE_RECOVERY_SERVICE_PUBLIC_KEY_ENTRY =
+ "CREATE TABLE " + RecoveryServiceMetadataEntry.TABLE_NAME + " ("
+ + RecoveryServiceMetadataEntry._ID + " INTEGER PRIMARY KEY,"
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " INTEGER,"
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " INTEGER,"
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY + " BLOB,"
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES + " TEXT,"
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMETERS + " INTEGER,"
+ + "UNIQUE("
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + ","
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_UID + "))";
+
+ private static final String SQL_DELETE_KEYS_ENTRY =
+ "DROP TABLE IF EXISTS " + KeysEntry.TABLE_NAME;
+
+ private static final String SQL_DELETE_USER_METADATA_ENTRY =
+ "DROP TABLE IF EXISTS " + UserMetadataEntry.TABLE_NAME;
+
+ private static final String SQL_DELETE_RECOVERY_SERVICE_PUBLIC_KEY_ENTRY =
+ "DROP TABLE IF EXISTS " + RecoveryServiceMetadataEntry.TABLE_NAME;
+
+ RecoverableKeyStoreDbHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(SQL_CREATE_KEYS_ENTRY);
+ db.execSQL(SQL_CREATE_USER_METADATA_ENTRY);
+ db.execSQL(SQL_CREATE_RECOVERY_SERVICE_PUBLIC_KEY_ENTRY);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL(SQL_DELETE_KEYS_ENTRY);
+ db.execSQL(SQL_DELETE_USER_METADATA_ENTRY);
+ db.execSQL(SQL_DELETE_RECOVERY_SERVICE_PUBLIC_KEY_ENTRY);
+ onCreate(db);
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
new file mode 100644
index 00000000..f7633e4c
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
@@ -0,0 +1,184 @@
+/*
+ * 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 com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import javax.security.auth.Destroyable;
+
+/**
+ * Stores pending recovery sessions in memory. We do not write these to disk, as it contains hashes
+ * of the user's lock screen.
+ *
+ * @hide
+ */
+public class RecoverySessionStorage implements Destroyable {
+
+ private final SparseArray<ArrayList<Entry>> mSessionsByUid = new SparseArray<>();
+
+ /**
+ * Returns the session for the given user with the given id.
+ *
+ * @param uid The uid of the recovery agent who created the session.
+ * @param sessionId The unique identifier for the session.
+ * @return The session info.
+ *
+ * @hide
+ */
+ @Nullable
+ public Entry get(int uid, String sessionId) {
+ ArrayList<Entry> userEntries = mSessionsByUid.get(uid);
+ if (userEntries == null) {
+ return null;
+ }
+ for (Entry entry : userEntries) {
+ if (sessionId.equals(entry.mSessionId)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a pending session for the given user.
+ *
+ * @param uid The uid of the recovery agent who created the session.
+ * @param entry The session info.
+ *
+ * @hide
+ */
+ public void add(int uid, Entry entry) {
+ if (mSessionsByUid.get(uid) == null) {
+ mSessionsByUid.put(uid, new ArrayList<>());
+ }
+ mSessionsByUid.get(uid).add(entry);
+ }
+
+ /**
+ * Removes all sessions associated with the given recovery agent uid.
+ *
+ * @param uid The uid of the recovery agent whose sessions to remove.
+ *
+ * @hide
+ */
+ public void remove(int uid) {
+ ArrayList<Entry> entries = mSessionsByUid.get(uid);
+ if (entries == null) {
+ return;
+ }
+ for (Entry entry : entries) {
+ entry.destroy();
+ }
+ mSessionsByUid.remove(uid);
+ }
+
+ /**
+ * Returns the total count of pending sessions.
+ *
+ * @hide
+ */
+ public int size() {
+ int size = 0;
+ int numberOfUsers = mSessionsByUid.size();
+ for (int i = 0; i < numberOfUsers; i++) {
+ ArrayList<Entry> entries = mSessionsByUid.valueAt(i);
+ size += entries.size();
+ }
+ return size;
+ }
+
+ /**
+ * Wipes the memory of any sensitive information (i.e., lock screen hashes and key claimants).
+ *
+ * @hide
+ */
+ @Override
+ public void destroy() {
+ int numberOfUids = mSessionsByUid.size();
+ for (int i = 0; i < numberOfUids; i++) {
+ ArrayList<Entry> entries = mSessionsByUid.valueAt(i);
+ for (Entry entry : entries) {
+ entry.destroy();
+ }
+ }
+ }
+
+ /**
+ * Information about a recovery session.
+ *
+ * @hide
+ */
+ public static class Entry implements Destroyable {
+ private final byte[] mLskfHash;
+ private final byte[] mKeyClaimant;
+ private final byte[] mVaultParams;
+ private final String mSessionId;
+
+ /**
+ * @hide
+ */
+ public Entry(String sessionId, byte[] lskfHash, byte[] keyClaimant, byte[] vaultParams) {
+ mLskfHash = lskfHash;
+ mSessionId = sessionId;
+ mKeyClaimant = keyClaimant;
+ mVaultParams = vaultParams;
+ }
+
+ /**
+ * Returns the hash of the lock screen associated with the recovery attempt.
+ *
+ * @hide
+ */
+ public byte[] getLskfHash() {
+ return mLskfHash;
+ }
+
+ /**
+ * Returns the key generated for this recovery attempt (used to decrypt data returned by
+ * the server).
+ *
+ * @hide
+ */
+ public byte[] getKeyClaimant() {
+ return mKeyClaimant;
+ }
+
+ /**
+ * Returns the vault params associated with the session.
+ *
+ * @hide
+ */
+ public byte[] getVaultParams() {
+ return mVaultParams;
+ }
+
+ /**
+ * Overwrites the memory for the lskf hash and key claimant.
+ *
+ * @hide
+ */
+ @Override
+ public void destroy() {
+ Arrays.fill(mLskfHash, (byte) 0);
+ Arrays.fill(mKeyClaimant, (byte) 0);
+ }
+ }
+}
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
new file mode 100644
index 00000000..d1a1629d
--- /dev/null
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.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 com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.annotation.Nullable;
+import android.security.recoverablekeystore.KeyStoreRecoveryData;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * In-memory storage for recovery snapshots.
+ *
+ * <p>Recovery snapshots are generated after a successful screen unlock. They are only generated if
+ * the recoverable keystore has been mutated since the previous snapshot. This class stores only the
+ * latest snapshot for each user.
+ *
+ * <p>This class is thread-safe. It is used both on the service thread and the
+ * {@link com.android.server.locksettings.recoverablekeystore.KeySyncTask} thread.
+ */
+public class RecoverySnapshotStorage {
+ @GuardedBy("this")
+ private final SparseArray<KeyStoreRecoveryData> mSnapshotByUserId = new SparseArray<>();
+
+ /**
+ * Sets the latest {@code snapshot} for the user {@code userId}.
+ */
+ public synchronized void put(int userId, KeyStoreRecoveryData snapshot) {
+ mSnapshotByUserId.put(userId, snapshot);
+ }
+
+ /**
+ * Returns the latest snapshot for user {@code userId}, or null if none exists.
+ */
+ @Nullable
+ public synchronized KeyStoreRecoveryData get(int userId) {
+ return mSnapshotByUserId.get(userId);
+ }
+
+ /**
+ * Removes any (if any) snapshot associated with user {@code userId}.
+ */
+ public synchronized void remove(int userId) {
+ mSnapshotByUserId.remove(userId);
+ }
+}
diff --git a/com/android/server/media/MediaRouterService.java b/com/android/server/media/MediaRouterService.java
index 0b089fbc..384efdda 100644
--- a/com/android/server/media/MediaRouterService.java
+++ b/com/android/server/media/MediaRouterService.java
@@ -21,6 +21,9 @@ import com.android.server.Watchdog;
import android.annotation.NonNull;
import android.app.ActivityManager;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -96,21 +99,23 @@ public final class MediaRouterService extends IMediaRouterService.Stub
private final ArrayMap<IBinder, ClientRecord> mAllClientRecords =
new ArrayMap<IBinder, ClientRecord>();
private int mCurrentUserId = -1;
- private boolean mGlobalBluetoothA2dpOn = false;
private final IAudioService mAudioService;
private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
private final Handler mHandler = new Handler();
- private final AudioRoutesInfo mAudioRoutesInfo = new AudioRoutesInfo();
private final IntArray mActivePlayerMinPriorityQueue = new IntArray();
private final IntArray mActivePlayerUidMinPriorityQueue = new IntArray();
+ private final BroadcastReceiver mReceiver = new MediaRouterServiceBroadcastReceiver();
+ BluetoothDevice mBluetoothDevice;
+ int mAudioRouteMainType = AudioRoutesInfo.MAIN_SPEAKER;
+ boolean mGlobalBluetoothA2dpOn = false;
+
public MediaRouterService(Context context) {
mContext = context;
Watchdog.getInstance().addMonitor(this);
mAudioService = IAudioService.Stub.asInterface(
ServiceManager.getService(Context.AUDIO_SERVICE));
-
mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
mAudioPlayerStateMonitor.registerListener(
new AudioPlayerStateMonitor.OnAudioPlayerActiveStateChangedListener() {
@@ -170,44 +175,30 @@ public final class MediaRouterService extends IMediaRouterService.Stub
@Override
public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
synchronized (mLock) {
- if (newRoutes.mainType != mAudioRoutesInfo.mainType) {
+ if (newRoutes.mainType != mAudioRouteMainType) {
if ((newRoutes.mainType & (AudioRoutesInfo.MAIN_HEADSET
| AudioRoutesInfo.MAIN_HEADPHONES
| AudioRoutesInfo.MAIN_USB)) == 0) {
// headset was plugged out.
- mGlobalBluetoothA2dpOn = newRoutes.bluetoothName != null;
+ mGlobalBluetoothA2dpOn = mBluetoothDevice != null;
} else {
// headset was plugged in.
mGlobalBluetoothA2dpOn = false;
}
- mAudioRoutesInfo.mainType = newRoutes.mainType;
- }
- if (!TextUtils.equals(
- newRoutes.bluetoothName, mAudioRoutesInfo.bluetoothName)) {
- if (newRoutes.bluetoothName == null) {
- // BT was disconnected.
- mGlobalBluetoothA2dpOn = false;
- } else {
- // BT was connected or changed.
- mGlobalBluetoothA2dpOn = true;
- }
- mAudioRoutesInfo.bluetoothName = newRoutes.bluetoothName;
+ mAudioRouteMainType = newRoutes.mainType;
}
- // Although a Bluetooth device is connected before a new audio playback is
- // started, dispatchAudioRoutChanged() can be called after
- // onAudioPlayerActiveStateChanged(). That causes restoreBluetoothA2dp()
- // is called before mGlobalBluetoothA2dpOn is updated.
- // Calling restoreBluetoothA2dp() here could prevent that.
- restoreBluetoothA2dp();
+ // The new audio routes info could be delivered with several seconds delay.
+ // In order to avoid such delay, Bluetooth device info will be updated
+ // via MediaRouterServiceBroadcastReceiver.
}
}
});
} catch (RemoteException e) {
Slog.w(TAG, "RemoteException in the audio service.");
}
- synchronized (mLock) {
- mGlobalBluetoothA2dpOn = (audioRoutes != null && audioRoutes.bluetoothName != null);
- }
+
+ IntentFilter intentFilter = new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ context.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null);
}
public void systemRunning() {
@@ -415,14 +406,12 @@ public final class MediaRouterService extends IMediaRouterService.Stub
void restoreBluetoothA2dp() {
try {
- boolean btConnected = false;
boolean a2dpOn = false;
synchronized (mLock) {
- btConnected = mAudioRoutesInfo.bluetoothName != null;
a2dpOn = mGlobalBluetoothA2dpOn;
}
// We don't need to change a2dp status when bluetooth is not connected.
- if (btConnected) {
+ if (mBluetoothDevice != null) {
Slog.v(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")");
mAudioService.setBluetoothA2dpOn(a2dpOn);
}
@@ -661,6 +650,25 @@ public final class MediaRouterService extends IMediaRouterService.Stub
return false;
}
+ final class MediaRouterServiceBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
+ int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
+ BluetoothProfile.STATE_DISCONNECTED);
+ if (state == BluetoothProfile.STATE_DISCONNECTED) {
+ mGlobalBluetoothA2dpOn = false;
+ mBluetoothDevice = null;
+ } else if (state == BluetoothProfile.STATE_CONNECTED) {
+ mGlobalBluetoothA2dpOn = true;
+ mBluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ // To ensure that BT A2DP is on, call restoreBluetoothA2dp().
+ restoreBluetoothA2dp();
+ }
+ }
+ }
+ }
+
/**
* Information about a particular client of the media router.
* The contents of this object is guarded by mLock.
diff --git a/com/android/server/net/NetworkStatsService.java b/com/android/server/net/NetworkStatsService.java
index 3af5265e..db61ef5c 100644
--- a/com/android/server/net/NetworkStatsService.java
+++ b/com/android/server/net/NetworkStatsService.java
@@ -873,6 +873,21 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
}
}
+ @Override
+ public long getUidStats(int uid, int type) {
+ return nativeGetUidStat(uid, type);
+ }
+
+ @Override
+ public long getIfaceStats(String iface, int type) {
+ return nativeGetIfaceStat(iface, type);
+ }
+
+ @Override
+ public long getTotalStats(int type) {
+ return nativeGetTotalStat(type);
+ }
+
/**
* Update {@link NetworkStatsRecorder} and {@link #mGlobalAlertBytes} to
* reflect current {@link #mPersistThreshold} value. Always defers to
@@ -1626,4 +1641,15 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
return getGlobalLong(NETSTATS_UID_TAG_PERSIST_BYTES, def);
}
}
+
+ private static int TYPE_RX_BYTES;
+ private static int TYPE_RX_PACKETS;
+ private static int TYPE_TX_BYTES;
+ private static int TYPE_TX_PACKETS;
+ private static int TYPE_TCP_RX_PACKETS;
+ private static int TYPE_TCP_TX_PACKETS;
+
+ 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/com/android/server/notification/ManagedServices.java b/com/android/server/notification/ManagedServices.java
index 019c7c2f..7d64aed4 100644
--- a/com/android/server/notification/ManagedServices.java
+++ b/com/android/server/notification/ManagedServices.java
@@ -39,8 +39,10 @@ import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.Build;
+import android.os.Handler;
import android.os.IBinder;
import android.os.IInterface;
+import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
@@ -86,6 +88,7 @@ abstract public class ManagedServices {
protected final String TAG = getClass().getSimpleName();
protected final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final int ON_BINDING_DIED_REBIND_DELAY_MS = 10000;
protected static final String ENABLED_SERVICES_SEPARATOR = ":";
/**
@@ -105,12 +108,15 @@ abstract public class ManagedServices {
private final IPackageManager mPm;
private final UserManager mUm;
private final Config mConfig;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
// contains connections to all connected services, including app services
// and system services
private final ArrayList<ManagedServiceInfo> mServices = new ArrayList<>();
// things that will be put into mServices as soon as they're ready
private final ArrayList<String> mServicesBinding = new ArrayList<>();
+ private final ArraySet<String> mServicesRebinding = new ArraySet<>();
+
// lists the component names of all enabled (and therefore potentially connected)
// app services for current profiles.
private ArraySet<ComponentName> mEnabledServicesForCurrentProfiles
@@ -890,6 +896,7 @@ abstract public class ManagedServices {
final String servicesBindingTag = name.toString() + "/" + userid;
if (mServicesBinding.contains(servicesBindingTag)) {
+ Slog.v(TAG, "Not registering " + name + " as bind is already in progress");
// stop registering this thing already! we're working on it
return;
}
@@ -938,6 +945,7 @@ abstract public class ManagedServices {
boolean added = false;
ManagedServiceInfo info = null;
synchronized (mMutex) {
+ mServicesRebinding.remove(servicesBindingTag);
mServicesBinding.remove(servicesBindingTag);
try {
mService = asInterface(binder);
@@ -959,6 +967,27 @@ abstract public class ManagedServices {
mServicesBinding.remove(servicesBindingTag);
Slog.v(TAG, getCaption() + " connection lost: " + name);
}
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ Slog.w(TAG, getCaption() + " binding died: " + name);
+ synchronized (mMutex) {
+ mServicesBinding.remove(servicesBindingTag);
+ mContext.unbindService(this);
+ if (!mServicesRebinding.contains(servicesBindingTag)) {
+ mServicesRebinding.add(servicesBindingTag);
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ registerService(name, userid);
+ }
+ }, ON_BINDING_DIED_REBIND_DELAY_MS);
+ } else {
+ Slog.v(TAG, getCaption() + " not rebinding as "
+ + "a previous rebind attempt was made: " + name);
+ }
+ }
+ }
};
if (!mContext.bindServiceAsUser(intent,
serviceConnection,
diff --git a/com/android/server/notification/NotificationManagerService.java b/com/android/server/notification/NotificationManagerService.java
index bec6fc2c..cf014007 100644
--- a/com/android/server/notification/NotificationManagerService.java
+++ b/com/android/server/notification/NotificationManagerService.java
@@ -4743,8 +4743,7 @@ public class NotificationManagerService extends SystemService {
}
void sendAccessibilityEvent(Notification notification, CharSequence packageName) {
- if (!mAccessibilityManager.isObservedEventType(
- AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)) {
+ if (!mAccessibilityManager.isEnabled()) {
return;
}
diff --git a/com/android/server/notification/ScheduleConditionProvider.java b/com/android/server/notification/ScheduleConditionProvider.java
index 50a51b29..ba7fe784 100644
--- a/com/android/server/notification/ScheduleConditionProvider.java
+++ b/com/android/server/notification/ScheduleConditionProvider.java
@@ -29,21 +29,22 @@ import android.os.Binder;
import android.provider.Settings;
import android.service.notification.Condition;
import android.service.notification.IConditionProvider;
+import android.service.notification.ScheduleCalendar;
import android.service.notification.ZenModeConfig;
-import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.notification.NotificationManagerService.DumpFilter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
-import java.util.TimeZone;
/**
* Built-in zen condition provider for daily scheduled time-based conditions.
@@ -62,10 +63,9 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
private static final String SEPARATOR = ";";
private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
-
private final Context mContext = this;
private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
- private ArraySet<Uri> mSnoozed = new ArraySet<>();
+ private ArraySet<Uri> mSnoozedForAlarm = new ArraySet<>();
private AlarmManager mAlarmManager;
private boolean mConnected;
@@ -102,7 +102,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
pw.println(mSubscriptions.get(conditionId).toString());
}
}
- pw.println(" snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozed));
+ pw.println(" snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozedForAlarm));
dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
}
@@ -129,11 +129,11 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
public void onSubscribe(Uri conditionId) {
if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) {
- notifyCondition(createCondition(conditionId, Condition.STATE_FALSE, "badCondition"));
+ notifyCondition(createCondition(conditionId, Condition.STATE_ERROR, "invalidId"));
return;
}
synchronized (mSubscriptions) {
- mSubscriptions.put(conditionId, toScheduleCalendar(conditionId));
+ mSubscriptions.put(conditionId, ZenModeConfig.toScheduleCalendar(conditionId));
}
evaluateSubscriptions();
}
@@ -169,32 +169,11 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
synchronized (mSubscriptions) {
setRegistered(!mSubscriptions.isEmpty());
for (Uri conditionId : mSubscriptions.keySet()) {
- final ScheduleCalendar cal = mSubscriptions.get(conditionId);
- if (cal != null && cal.isInSchedule(now)) {
- if (conditionSnoozed(conditionId) || cal.shouldExitForAlarm(now)) {
- conditionsToNotify.add(createCondition(
- conditionId, Condition.STATE_FALSE, "alarmCanceled"));
- addSnoozed(conditionId);
- } else {
- conditionsToNotify.add(createCondition(
- conditionId, Condition.STATE_TRUE, "meetsSchedule"));
- }
- cal.maybeSetNextAlarm(now, nextUserAlarmTime);
- } else {
- conditionsToNotify.add(createCondition(
- conditionId, Condition.STATE_FALSE, "!meetsSchedule"));
- removeSnoozed(conditionId);
- if (cal != null && nextUserAlarmTime == 0) {
- cal.maybeSetNextAlarm(now, nextUserAlarmTime);
- }
- }
- if (cal != null) {
- final long nextChangeTime = cal.getNextChangeTime(now);
- if (nextChangeTime > 0 && nextChangeTime > now) {
- if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
- mNextAlarmTime = nextChangeTime;
- }
- }
+ Condition condition =
+ evaluateSubscriptionLocked(conditionId, mSubscriptions.get(conditionId),
+ now, nextUserAlarmTime);
+ if (condition != null) {
+ conditionsToNotify.add(condition);
}
}
}
@@ -202,6 +181,39 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
updateAlarm(now, mNextAlarmTime);
}
+ @VisibleForTesting
+ @GuardedBy("mSubscriptions")
+ Condition evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal,
+ long now, long nextUserAlarmTime) {
+ Condition condition;
+ if (cal == null) {
+ condition = createCondition(conditionId, Condition.STATE_ERROR, "!invalidId");
+ removeSnoozed(conditionId);
+ return condition;
+ }
+ if (cal.isInSchedule(now)) {
+ if (conditionSnoozed(conditionId)) {
+ condition = createCondition(conditionId, Condition.STATE_FALSE, "snoozed");
+ } else if (cal.shouldExitForAlarm(now)) {
+ condition = createCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
+ addSnoozed(conditionId);
+ } else {
+ condition = createCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
+ }
+ } else {
+ condition = createCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
+ removeSnoozed(conditionId);
+ }
+ cal.maybeSetNextAlarm(now, nextUserAlarmTime);
+ final long nextChangeTime = cal.getNextChangeTime(now);
+ if (nextChangeTime > 0 && nextChangeTime > now) {
+ if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
+ mNextAlarmTime = nextChangeTime;
+ }
+ }
+ return condition;
+ }
+
private void updateAlarm(long now, long time) {
final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
@@ -230,15 +242,6 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
return cal != null && cal.isInSchedule(time);
}
- private 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 void setRegistered(boolean registered) {
if (mRegistered == registered) return;
if (DEBUG) Slog.d(TAG, "setRegistered " + registered);
@@ -266,27 +269,28 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
}
private boolean conditionSnoozed(Uri conditionId) {
- synchronized (mSnoozed) {
- return mSnoozed.contains(conditionId);
+ synchronized (mSnoozedForAlarm) {
+ return mSnoozedForAlarm.contains(conditionId);
}
}
- private void addSnoozed(Uri conditionId) {
- synchronized (mSnoozed) {
- mSnoozed.add(conditionId);
+ @VisibleForTesting
+ void addSnoozed(Uri conditionId) {
+ synchronized (mSnoozedForAlarm) {
+ mSnoozedForAlarm.add(conditionId);
saveSnoozedLocked();
}
}
private void removeSnoozed(Uri conditionId) {
- synchronized (mSnoozed) {
- mSnoozed.remove(conditionId);
+ synchronized (mSnoozedForAlarm) {
+ mSnoozedForAlarm.remove(conditionId);
saveSnoozedLocked();
}
}
- public void saveSnoozedLocked() {
- final String setting = TextUtils.join(SEPARATOR, mSnoozed);
+ private void saveSnoozedLocked() {
+ final String setting = TextUtils.join(SEPARATOR, mSnoozedForAlarm);
final int currentUser = ActivityManager.getCurrentUser();
Settings.Secure.putStringForUser(mContext.getContentResolver(),
SCP_SETTING,
@@ -294,8 +298,8 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
currentUser);
}
- public void readSnoozed() {
- synchronized (mSnoozed) {
+ private void readSnoozed() {
+ synchronized (mSnoozedForAlarm) {
long identity = Binder.clearCallingIdentity();
try {
final String setting = Settings.Secure.getStringForUser(
@@ -312,7 +316,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
if (TextUtils.isEmpty(token)) {
continue;
}
- mSnoozed.add(Uri.parse(token));
+ mSnoozedForAlarm.add(Uri.parse(token));
}
}
} finally {
diff --git a/com/android/server/notification/ZenModeHelper.java b/com/android/server/notification/ZenModeHelper.java
index 1e9fab5c..700ccad8 100644
--- a/com/android/server/notification/ZenModeHelper.java
+++ b/com/android/server/notification/ZenModeHelper.java
@@ -98,11 +98,6 @@ public class ZenModeHelper {
private final Metrics mMetrics = new Metrics();
private final ConditionProviders.Config mServiceConfig;
- protected final ArrayList<String> mDefaultRuleIds = new ArrayList<>();
- private final String EVENTS_DEFAULT_RULE = "EVENTS_DEFAULT_RULE";
- private final String SCHEDULED_DEFAULT_RULE_1 = "SCHEDULED_DEFAULT_RULE_1";
- private final String SCHEDULED_DEFAULT_RULE_2 = "SCHEDULED_DEFAULT_RULE_2";
-
@VisibleForTesting protected int mZenMode;
private int mUser = UserHandle.USER_SYSTEM;
@VisibleForTesting protected ZenModeConfig mConfig;
@@ -115,9 +110,8 @@ public class ZenModeHelper {
public static final long SUPPRESSED_EFFECT_ALL = SUPPRESSED_EFFECT_CALLS
| SUPPRESSED_EFFECT_NOTIFICATIONS;
- protected String mDefaultRuleWeeknightsName;
+ protected String mDefaultRuleEveryNightName;
protected String mDefaultRuleEventsName;
- protected String mDefaultRuleWeekendsName;
public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders) {
mContext = context;
@@ -230,12 +224,25 @@ public class ZenModeHelper {
config = mDefaultConfig.copy();
config.user = user;
}
+ enforceDefaultRulesExist(config);
synchronized (mConfig) {
setConfigLocked(config, reason);
}
cleanUpZenRules();
}
+ private void enforceDefaultRulesExist(ZenModeConfig config) {
+ for (String id : ZenModeConfig.DEFAULT_RULE_IDS) {
+ if (!config.automaticRules.containsKey(id)) {
+ if (id.equals(ZenModeConfig.EVENTS_DEFAULT_RULE_ID)) {
+ appendDefaultEventRules(config);
+ } else if (id.equals(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID)) {
+ appendDefaultEveryNightRule(config);
+ }
+ }
+ }
+ }
+
public int getZenModeListenerInterruptionFilter() {
return NotificationManager.zenModeToInterruptionFilter(mZenMode);
}
@@ -421,17 +428,12 @@ public class ZenModeHelper {
public void setDefaultZenRules(Context context) {
mDefaultConfig = readDefaultConfig(context.getResources());
-
- mDefaultRuleIds.add(EVENTS_DEFAULT_RULE);
- mDefaultRuleIds.add(SCHEDULED_DEFAULT_RULE_1);
- mDefaultRuleIds.add(SCHEDULED_DEFAULT_RULE_2);
-
appendDefaultRules(mDefaultConfig);
}
private void appendDefaultRules (ZenModeConfig config) {
getDefaultRuleNames();
- appendDefaultScheduleRules(config);
+ appendDefaultEveryNightRule(config);
appendDefaultEventRules(config);
}
@@ -450,7 +452,7 @@ public class ZenModeHelper {
protected void updateDefaultZenRules() {
ZenModeConfig configDefaultRules = new ZenModeConfig();
appendDefaultRules(configDefaultRules); // "new" localized default rules
- for (String ruleId : mDefaultRuleIds) {
+ for (String ruleId : ZenModeConfig.DEFAULT_RULE_IDS) {
AutomaticZenRule currRule = getAutomaticZenRule(ruleId);
ZenRule defaultRule = configDefaultRules.automaticRules.get(ruleId);
// if default rule wasn't customized, use localized name instead of previous
@@ -812,10 +814,8 @@ public class ZenModeHelper {
private void getDefaultRuleNames() {
// on locale-change, these values differ
- mDefaultRuleWeeknightsName = mContext.getResources()
- .getString(R.string.zen_mode_default_weeknights_name);
- mDefaultRuleWeekendsName = mContext.getResources()
- .getString(R.string.zen_mode_default_weekends_name);
+ mDefaultRuleEveryNightName = mContext.getResources()
+ .getString(R.string.zen_mode_default_every_night_name);
mDefaultRuleEventsName = mContext.getResources()
.getString(R.string.zen_mode_default_events_name);
}
@@ -935,39 +935,23 @@ public class ZenModeHelper {
return new ZenModeConfig();
}
- private void appendDefaultScheduleRules(ZenModeConfig config) {
+ private void appendDefaultEveryNightRule(ZenModeConfig config) {
if (config == null) return;
final ScheduleInfo weeknights = new ScheduleInfo();
- weeknights.days = ZenModeConfig.WEEKNIGHT_DAYS;
+ weeknights.days = ZenModeConfig.ALL_DAYS;
weeknights.startHour = 22;
weeknights.endHour = 7;
weeknights.exitAtAlarm = true;
- final ZenRule rule1 = new ZenRule();
- rule1.enabled = false;
- rule1.name = mDefaultRuleWeeknightsName;
- rule1.conditionId = ZenModeConfig.toScheduleConditionId(weeknights);
- rule1.zenMode = Global.ZEN_MODE_ALARMS;
- rule1.component = ScheduleConditionProvider.COMPONENT;
- rule1.id = SCHEDULED_DEFAULT_RULE_1;
- rule1.creationTime = System.currentTimeMillis();
- config.automaticRules.put(rule1.id, rule1);
-
- final ScheduleInfo weekends = new ScheduleInfo();
- weekends.days = ZenModeConfig.WEEKEND_DAYS;
- weekends.startHour = 23;
- weekends.startMinute = 30;
- weekends.endHour = 10;
- weekends.exitAtAlarm = true;
- final ZenRule rule2 = new ZenRule();
- rule2.enabled = false;
- rule2.name = mDefaultRuleWeekendsName;
- rule2.conditionId = ZenModeConfig.toScheduleConditionId(weekends);
- rule2.zenMode = Global.ZEN_MODE_ALARMS;
- rule2.component = ScheduleConditionProvider.COMPONENT;
- rule2.id = SCHEDULED_DEFAULT_RULE_2;
- rule2.creationTime = System.currentTimeMillis();
- config.automaticRules.put(rule2.id, rule2);
+ final ZenRule rule = new ZenRule();
+ rule.enabled = false;
+ rule.name = mDefaultRuleEveryNightName;
+ rule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights);
+ rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ rule.component = ScheduleConditionProvider.COMPONENT;
+ rule.id = ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID;
+ rule.creationTime = System.currentTimeMillis();
+ config.automaticRules.put(rule.id, rule);
}
private void appendDefaultEventRules(ZenModeConfig config) {
@@ -980,9 +964,9 @@ public class ZenModeHelper {
rule.enabled = false;
rule.name = mDefaultRuleEventsName;
rule.conditionId = ZenModeConfig.toEventConditionId(events);
- rule.zenMode = Global.ZEN_MODE_ALARMS;
+ rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
rule.component = EventConditionProvider.COMPONENT;
- rule.id = EVENTS_DEFAULT_RULE;
+ rule.id = ZenModeConfig.EVENTS_DEFAULT_RULE_ID;
rule.creationTime = System.currentTimeMillis();
config.automaticRules.put(rule.id, rule);
}
diff --git a/com/android/server/oemlock/OemLockService.java b/com/android/server/oemlock/OemLockService.java
index 5b3d1eca..2a2ff06b 100644
--- a/com/android/server/oemlock/OemLockService.java
+++ b/com/android/server/oemlock/OemLockService.java
@@ -178,14 +178,21 @@ public class OemLockService extends SystemService {
}
}
+ /** Currently MasterClearConfirm will call isOemUnlockAllowed()
+ * to sync PersistentDataBlockOemUnlockAllowedBit which
+ * is needed before factory reset
+ * TODO: Figure out better place to run sync e.g. adding new API
+ */
@Override
public boolean isOemUnlockAllowed() {
enforceOemUnlockReadPermission();
final long token = Binder.clearCallingIdentity();
try {
- return mOemLock.isOemUnlockAllowedByCarrier() &&
- mOemLock.isOemUnlockAllowedByDevice();
+ boolean allowed = mOemLock.isOemUnlockAllowedByCarrier()
+ && mOemLock.isOemUnlockAllowedByDevice();
+ setPersistentDataBlockOemUnlockAllowedBit(allowed);
+ return allowed;
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -213,7 +220,8 @@ public class OemLockService extends SystemService {
final PersistentDataBlockManager pdbm = (PersistentDataBlockManager)
mContext.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE);
// if mOemLock is PersistentDataBlockLock, then the bit should have already been set
- if (pdbm != null && !(mOemLock instanceof PersistentDataBlockLock)) {
+ if (pdbm != null && !(mOemLock instanceof PersistentDataBlockLock)
+ && pdbm.getOemUnlockEnabled() != allowed) {
Slog.i(TAG, "Update OEM Unlock bit in pst partition to " + allowed);
pdbm.setOemUnlockEnabled(allowed);
}
diff --git a/com/android/server/om/OverlayManagerService.java b/com/android/server/om/OverlayManagerService.java
index 2940a6e3..2041de64 100644
--- a/com/android/server/om/OverlayManagerService.java
+++ b/com/android/server/om/OverlayManagerService.java
@@ -370,10 +370,10 @@ public final class OverlayManagerService extends SystemService {
false);
if (pi != null) {
mPackageManager.cachePackageInfo(packageName, userId, pi);
- if (!isOverlayPackage(pi)) {
- mImpl.onTargetPackageAdded(packageName, userId);
- } else {
+ if (pi.isOverlayPackage()) {
mImpl.onOverlayPackageAdded(packageName, userId);
+ } else {
+ mImpl.onTargetPackageAdded(packageName, userId);
}
}
}
@@ -388,10 +388,10 @@ public final class OverlayManagerService extends SystemService {
false);
if (pi != null) {
mPackageManager.cachePackageInfo(packageName, userId, pi);
- if (!isOverlayPackage(pi)) {
- mImpl.onTargetPackageChanged(packageName, userId);
- } else {
+ if (pi.isOverlayPackage()) {
mImpl.onOverlayPackageChanged(packageName, userId);
+ } else {
+ mImpl.onTargetPackageChanged(packageName, userId);
}
}
}
@@ -404,10 +404,10 @@ public final class OverlayManagerService extends SystemService {
synchronized (mLock) {
mPackageManager.forgetPackageInfo(packageName, userId);
final OverlayInfo oi = mImpl.getOverlayInfo(packageName, userId);
- if (oi == null) {
- mImpl.onTargetPackageUpgrading(packageName, userId);
- } else {
+ if (oi != null) {
mImpl.onOverlayPackageUpgrading(packageName, userId);
+ } else {
+ mImpl.onTargetPackageUpgrading(packageName, userId);
}
}
}
@@ -421,10 +421,10 @@ public final class OverlayManagerService extends SystemService {
false);
if (pi != null) {
mPackageManager.cachePackageInfo(packageName, userId, pi);
- if (!isOverlayPackage(pi)) {
- mImpl.onTargetPackageUpgraded(packageName, userId);
- } else {
+ if (pi.isOverlayPackage()) {
mImpl.onOverlayPackageUpgraded(packageName, userId);
+ } else {
+ mImpl.onTargetPackageUpgraded(packageName, userId);
}
}
}
@@ -437,10 +437,10 @@ public final class OverlayManagerService extends SystemService {
synchronized (mLock) {
mPackageManager.forgetPackageInfo(packageName, userId);
final OverlayInfo oi = mImpl.getOverlayInfo(packageName, userId);
- if (oi == null) {
- mImpl.onTargetPackageRemoved(packageName, userId);
- } else {
+ if (oi != null) {
mImpl.onOverlayPackageRemoved(packageName, userId);
+ } else {
+ mImpl.onTargetPackageRemoved(packageName, userId);
}
}
}
@@ -668,10 +668,6 @@ public final class OverlayManagerService extends SystemService {
}
};
- private boolean isOverlayPackage(@NonNull final PackageInfo pi) {
- return pi != null && pi.overlayTarget != null;
- }
-
private final class OverlayChangeListener
implements OverlayManagerServiceImpl.OverlayChangeListener {
@Override
diff --git a/com/android/server/om/OverlayManagerServiceImpl.java b/com/android/server/om/OverlayManagerServiceImpl.java
index db6e9749..253d4f5b 100644
--- a/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/com/android/server/om/OverlayManagerServiceImpl.java
@@ -102,11 +102,11 @@ final class OverlayManagerServiceImpl {
mSettings.init(overlayPackage.packageName, newUserId,
overlayPackage.overlayTarget,
overlayPackage.applicationInfo.getBaseCodePath(),
- overlayPackage.isStaticOverlay, overlayPackage.overlayPriority);
+ overlayPackage.isStaticOverlayPackage(), overlayPackage.overlayPriority);
if (oi == null) {
// This overlay does not exist in our settings.
- if (overlayPackage.isStaticOverlay ||
+ if (overlayPackage.isStaticOverlayPackage() ||
mDefaultOverlays.contains(overlayPackage.packageName)) {
// Enable this overlay by default.
if (DEBUG) {
@@ -255,8 +255,8 @@ final class OverlayManagerServiceImpl {
mPackageManager.getPackageInfo(overlayPackage.overlayTarget, userId);
mSettings.init(packageName, userId, overlayPackage.overlayTarget,
- overlayPackage.applicationInfo.getBaseCodePath(), overlayPackage.isStaticOverlay,
- overlayPackage.overlayPriority);
+ overlayPackage.applicationInfo.getBaseCodePath(),
+ overlayPackage.isStaticOverlayPackage(), overlayPackage.overlayPriority);
try {
if (updateState(targetPackage, overlayPackage, userId)) {
mListener.onOverlaysChanged(overlayPackage.overlayTarget, userId);
@@ -313,7 +313,7 @@ final class OverlayManagerServiceImpl {
}
// Ignore static overlays.
- if (overlayPackage.isStaticOverlay) {
+ if (overlayPackage.isStaticOverlayPackage()) {
return false;
}
@@ -363,7 +363,7 @@ final class OverlayManagerServiceImpl {
continue;
}
- if (disabledOverlayPackageInfo.isStaticOverlay) {
+ if (disabledOverlayPackageInfo.isStaticOverlayPackage()) {
// Don't touch static overlays.
continue;
}
@@ -388,7 +388,7 @@ final class OverlayManagerServiceImpl {
private boolean isPackageUpdatableOverlay(@NonNull final String packageName, final int userId) {
final PackageInfo overlayPackage = mPackageManager.getPackageInfo(packageName, userId);
- if (overlayPackage == null || overlayPackage.isStaticOverlay) {
+ if (overlayPackage == null || overlayPackage.isStaticOverlayPackage()) {
return false;
}
return true;
@@ -483,7 +483,8 @@ final class OverlayManagerServiceImpl {
throws OverlayManagerSettings.BadKeyException {
// Static RROs targeting to "android", ie framework-res.apk, are handled by native layers.
if (targetPackage != null &&
- !("android".equals(targetPackage.packageName) && overlayPackage.isStaticOverlay)) {
+ !("android".equals(targetPackage.packageName)
+ && overlayPackage.isStaticOverlayPackage())) {
mIdmapManager.createIdmap(targetPackage, overlayPackage, userId);
}
diff --git a/com/android/server/pm/DumpState.java b/com/android/server/pm/DumpState.java
index 7ebef83a..f4ee0ce7 100644
--- a/com/android/server/pm/DumpState.java
+++ b/com/android/server/pm/DumpState.java
@@ -40,6 +40,7 @@ public final class DumpState {
public static final int DUMP_COMPILER_STATS = 1 << 21;
public static final int DUMP_CHANGES = 1 << 22;
public static final int DUMP_VOLUMES = 1 << 23;
+ public static final int DUMP_SERVICE_PERMISSIONS = 1 << 24;
public static final int OPTION_SHOW_FILTERS = 1 << 0;
@@ -92,4 +93,4 @@ public final class DumpState {
public void setSharedUser(SharedUserSetting user) {
mSharedUser = user;
}
-} \ No newline at end of file
+}
diff --git a/com/android/server/pm/Installer.java b/com/android/server/pm/Installer.java
index 6a06be2f..be66fe22 100644
--- a/com/android/server/pm/Installer.java
+++ b/com/android/server/pm/Installer.java
@@ -496,6 +496,26 @@ public class Installer extends SystemService {
}
}
+ public boolean createProfileSnapshot(int appId, String packageName, String codePath)
+ throws InstallerException {
+ if (!checkBeforeRemote()) return false;
+ try {
+ return mInstalld.createProfileSnapshot(appId, packageName, codePath);
+ } catch (Exception e) {
+ throw InstallerException.from(e);
+ }
+ }
+
+ public void destroyProfileSnapshot(String packageName, String codePath)
+ throws InstallerException {
+ if (!checkBeforeRemote()) return;
+ try {
+ mInstalld.destroyProfileSnapshot(packageName, codePath);
+ } catch (Exception e) {
+ throw InstallerException.from(e);
+ }
+ }
+
public void invalidateMounts() throws InstallerException {
if (!checkBeforeRemote()) return;
try {
diff --git a/com/android/server/pm/InstantAppResolver.java b/com/android/server/pm/InstantAppResolver.java
index 5f54c67a..30072d45 100644
--- a/com/android/server/pm/InstantAppResolver.java
+++ b/com/android/server/pm/InstantAppResolver.java
@@ -40,14 +40,11 @@ import android.content.pm.InstantAppIntentFilter;
import android.content.pm.InstantAppResolveInfo;
import android.content.pm.InstantAppResolveInfo.InstantAppDigest;
import android.metrics.LogMaker;
-import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
-import android.util.Pair;
-import android.util.Slog;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
@@ -56,11 +53,9 @@ import com.android.server.pm.EphemeralResolverConnection.PhaseTwoCallback;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
-import java.util.concurrent.TimeoutException;
/** @hide */
public abstract class InstantAppResolver {
@@ -159,8 +154,9 @@ public abstract class InstantAppResolver {
long startTime) {
final String packageName;
final String splitName;
- final int versionCode;
+ final long versionCode;
final Intent failureIntent;
+ final Bundle extras;
if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
final AuxiliaryResolveInfo instantAppIntentInfo =
InstantAppResolver.filterInstantAppIntent(
@@ -172,17 +168,20 @@ public abstract class InstantAppResolver {
splitName = instantAppIntentInfo.splitName;
versionCode = instantAppIntentInfo.resolveInfo.getVersionCode();
failureIntent = instantAppIntentInfo.failureIntent;
+ extras = instantAppIntentInfo.resolveInfo.getExtras();
} else {
packageName = null;
splitName = null;
versionCode = -1;
failureIntent = null;
+ extras = null;
}
} else {
packageName = null;
splitName = null;
versionCode = -1;
failureIntent = null;
+ extras = null;
}
final Intent installerIntent = buildEphemeralInstallerIntent(
Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE,
@@ -197,6 +196,7 @@ public abstract class InstantAppResolver {
requestObj.responseObj.installFailureActivity,
versionCode,
token,
+ extras,
false /*needsPhaseTwo*/);
installerIntent.setComponent(new ComponentName(
instantAppInstaller.packageName, instantAppInstaller.name));
@@ -241,8 +241,9 @@ public abstract class InstantAppResolver {
@NonNull String instantAppPackageName,
@Nullable String instantAppSplitName,
@Nullable ComponentName installFailureActivity,
- int versionCode,
+ long versionCode,
@Nullable String token,
+ @Nullable Bundle extras,
boolean needsPhaseTwo) {
// Construct the intent that launches the instant installer
int flags = origIntent.getFlags();
@@ -258,6 +259,10 @@ public abstract class InstantAppResolver {
if (origIntent.getData() != null) {
intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost());
}
+ intent.putExtra(Intent.EXTRA_INSTANT_APP_ACTION, origIntent.getAction());
+ if (extras != null) {
+ intent.putExtra(Intent.EXTRA_INSTANT_APP_EXTRAS, extras);
+ }
// We have all of the data we need; just start the installer without a second phase
if (!needsPhaseTwo) {
@@ -307,7 +312,8 @@ public abstract class InstantAppResolver {
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, instantAppPackageName);
intent.putExtra(Intent.EXTRA_SPLIT_NAME, instantAppSplitName);
- intent.putExtra(Intent.EXTRA_VERSION_CODE, versionCode);
+ intent.putExtra(Intent.EXTRA_VERSION_CODE, (int) (versionCode & 0x7fffffff));
+ intent.putExtra(Intent.EXTRA_LONG_VERSION_CODE, versionCode);
intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage);
if (verificationBundle != null) {
intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle);
diff --git a/com/android/server/pm/PackageDexOptimizer.java b/com/android/server/pm/PackageDexOptimizer.java
index 00cfa310..730a9fda 100644
--- a/com/android/server/pm/PackageDexOptimizer.java
+++ b/com/android/server/pm/PackageDexOptimizer.java
@@ -128,6 +128,10 @@ public class PackageDexOptimizer {
int performDexOpt(PackageParser.Package pkg, String[] sharedLibraries,
String[] instructionSets, CompilerStats.PackageStats packageStats,
PackageDexUsage.PackageUseInfo packageUseInfo, DexoptOptions options) {
+ if (pkg.applicationInfo.uid == -1) {
+ throw new IllegalArgumentException("Dexopt for " + pkg.packageName
+ + " has invalid uid.");
+ }
if (!canOptimizePackage(pkg)) {
return DEX_OPT_SKIPPED;
}
@@ -299,6 +303,9 @@ public class PackageDexOptimizer {
*/
public int dexOptSecondaryDexPath(ApplicationInfo info, String path,
PackageDexUsage.DexUseInfo dexUseInfo, DexoptOptions options) {
+ if (info.uid == -1) {
+ throw new IllegalArgumentException("Dexopt for path " + path + " has invalid uid.");
+ }
synchronized (mInstallLock) {
final long acquireTime = acquireWakeLockLI(info.uid);
try {
diff --git a/com/android/server/pm/PackageInstallerService.java b/com/android/server/pm/PackageInstallerService.java
index be9b2f36..14128a78 100644
--- a/com/android/server/pm/PackageInstallerService.java
+++ b/com/android/server/pm/PackageInstallerService.java
@@ -27,7 +27,8 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.app.PackageDeleteObserver;
import android.app.PackageInstallObserver;
-import android.app.admin.DevicePolicyManager;
+import android.app.admin.DeviceAdminInfo;
+import android.app.admin.DevicePolicyManagerInternal;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
@@ -704,20 +705,25 @@ public class PackageInstallerService extends IPackageInstaller.Stub {
mAppOps.checkPackage(callingUid, callerPackageName);
}
- // Check whether the caller is device owner, in which case we do it silently.
- DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(
- Context.DEVICE_POLICY_SERVICE);
- boolean isDeviceOwner = (dpm != null) && dpm.isDeviceOwnerAppOnCallingUser(
- callerPackageName);
+ // Check whether the caller is device owner or affiliated profile owner, in which case we do
+ // it silently.
+ final int callingUserId = UserHandle.getUserId(callingUid);
+ DevicePolicyManagerInternal dpmi =
+ LocalServices.getService(DevicePolicyManagerInternal.class);
+ final boolean isDeviceOwnerOrAffiliatedProfileOwner =
+ dpmi != null && dpmi.isActiveAdminWithPolicy(callingUid,
+ DeviceAdminInfo.USES_POLICY_PROFILE_OWNER)
+ && dpmi.isUserAffiliatedWithDevice(callingUserId);
final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
- statusReceiver, versionedPackage.getPackageName(), isDeviceOwner, userId);
+ statusReceiver, versionedPackage.getPackageName(),
+ isDeviceOwnerOrAffiliatedProfileOwner, userId);
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DELETE_PACKAGES)
== PackageManager.PERMISSION_GRANTED) {
// Sweet, call straight through!
mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags);
- } else if (isDeviceOwner) {
- // Allow the DeviceOwner to silently delete packages
+ } else if (isDeviceOwnerOrAffiliatedProfileOwner) {
+ // Allow the device owner and affiliated profile owner to silently delete packages
// Need to clear the calling identity to get DELETE_PACKAGES permission
long ident = Binder.clearCallingIdentity();
try {
diff --git a/com/android/server/pm/PackageInstallerSession.java b/com/android/server/pm/PackageInstallerSession.java
index 0e11cc79..5cf08dc4 100644
--- a/com/android/server/pm/PackageInstallerSession.java
+++ b/com/android/server/pm/PackageInstallerSession.java
@@ -41,7 +41,8 @@ import static com.android.server.pm.PackageInstallerService.prepareStageDir;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.admin.DevicePolicyManager;
+import android.app.admin.DeviceAdminInfo;
+import android.app.admin.DevicePolicyManagerInternal;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
@@ -91,6 +92,7 @@ import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
+import com.android.server.LocalServices;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.PackageInstallerService.PackageInstallObserverAdapter;
@@ -223,7 +225,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
@GuardedBy("mLock")
private String mPackageName;
@GuardedBy("mLock")
- private int mVersionCode;
+ private long mVersionCode;
@GuardedBy("mLock")
private Signature[] mSignatures;
@GuardedBy("mLock")
@@ -311,14 +313,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
};
/**
- * @return {@code true} iff the installing is app an device owner?
+ * @return {@code true} iff the installing is app an device owner or affiliated profile owner.
*/
- private boolean isInstallerDeviceOwnerLocked() {
- DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(
- Context.DEVICE_POLICY_SERVICE);
-
- return (dpm != null) && dpm.isDeviceOwnerAppOnCallingUser(
- mInstallerPackageName);
+ private boolean isInstallerDeviceOwnerOrAffiliatedProfileOwnerLocked() {
+ DevicePolicyManagerInternal dpmi =
+ LocalServices.getService(DevicePolicyManagerInternal.class);
+ return dpmi != null && dpmi.isActiveAdminWithPolicy(mInstallerUid,
+ DeviceAdminInfo.USES_POLICY_PROFILE_OWNER) && dpmi.isUserAffiliatedWithDevice(
+ userId);
}
/**
@@ -347,10 +349,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
final boolean forcePermissionPrompt =
(params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0;
- // Device owners are allowed to silently install packages, so the permission check is
- // waived if the installer is the device owner.
+ // Device owners and affiliated profile owners are allowed to silently install packages, so
+ // the permission check is waived if the installer is the device owner.
return forcePermissionPrompt || !(isPermissionGranted || isInstallerRoot
- || isInstallerDeviceOwnerLocked());
+ || isInstallerDeviceOwnerOrAffiliatedProfileOwnerLocked());
}
public PackageInstallerSession(PackageInstallerService.InternalCallback callback,
@@ -705,7 +707,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
assertPreparedAndNotDestroyedLocked("commit");
final PackageInstallObserverAdapter adapter = new PackageInstallObserverAdapter(
- mContext, statusReceiver, sessionId, isInstallerDeviceOwnerLocked(), userId);
+ mContext, statusReceiver, sessionId,
+ isInstallerDeviceOwnerOrAffiliatedProfileOwnerLocked(), userId);
mRemoteObserver = adapter.getBinder();
if (forTransfer) {
@@ -1003,7 +1006,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
// Use first package to define unknown values
if (mPackageName == null) {
mPackageName = apk.packageName;
- mVersionCode = apk.versionCode;
+ mVersionCode = apk.getLongVersionCode();
}
if (mSignatures == null) {
mSignatures = apk.signatures;
@@ -1054,7 +1057,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
// ensure we've got appropriate package name, version code and signatures
if (mPackageName == null) {
mPackageName = pkgInfo.packageName;
- mVersionCode = pkgInfo.versionCode;
+ mVersionCode = pkgInfo.getLongVersionCode();
}
if (mSignatures == null) {
mSignatures = pkgInfo.signatures;
@@ -1146,7 +1149,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
+ " specified package " + params.appPackageName
+ " inconsistent with " + apk.packageName);
}
- if (mVersionCode != apk.versionCode) {
+ if (mVersionCode != apk.getLongVersionCode()) {
throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag
+ " version code " + apk.versionCode + " inconsistent with "
+ mVersionCode);
diff --git a/com/android/server/pm/PackageManagerService.java b/com/android/server/pm/PackageManagerService.java
index 2d5f7c71..1157af41 100644
--- a/com/android/server/pm/PackageManagerService.java
+++ b/com/android/server/pm/PackageManagerService.java
@@ -47,6 +47,7 @@ import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SHARED_LIBRARY;
+import static android.content.pm.PackageManager.INSTALL_FAILED_NEWER_SDK;
import static android.content.pm.PackageManager.INSTALL_FAILED_PACKAGE_CHANGED;
import static android.content.pm.PackageManager.INSTALL_FAILED_REPLACE_COULDNT_DELETE;
import static android.content.pm.PackageManager.INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE;
@@ -111,7 +112,6 @@ import static com.android.server.pm.PackageManagerServiceUtils.verifySignatures;
import static com.android.server.pm.permission.PermissionsState.PERMISSION_OPERATION_FAILURE;
import static com.android.server.pm.permission.PermissionsState.PERMISSION_OPERATION_SUCCESS;
import static com.android.server.pm.permission.PermissionsState.PERMISSION_OPERATION_SUCCESS_GIDS_CHANGED;
-import static dalvik.system.DexFile.getNonProfileGuidedCompilerFilter;
import android.Manifest;
import android.annotation.IntDef;
@@ -163,16 +163,19 @@ import android.content.pm.PackageCleanItem;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInfoLite;
import android.content.pm.PackageInstaller;
+import android.content.pm.PackageList;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.PackageManager.LegacyPackageDeleteObserver;
import android.content.pm.PackageManager.PackageInfoFlags;
+import android.content.pm.PackageManagerInternal.PackageListObserver;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.ActivityIntentInfo;
import android.content.pm.PackageParser.Package;
import android.content.pm.PackageParser.PackageLite;
import android.content.pm.PackageParser.PackageParserException;
import android.content.pm.PackageParser.ParseFlags;
+import android.content.pm.PackageParser.ServiceIntentInfo;
import android.content.pm.PackageStats;
import android.content.pm.PackageUserState;
import android.content.pm.ParceledListSlice;
@@ -187,6 +190,7 @@ import android.content.pm.UserInfo;
import android.content.pm.VerifierDeviceIdentity;
import android.content.pm.VerifierInfo;
import android.content.pm.VersionedPackage;
+import android.content.pm.dex.IArtManager;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Bitmap;
@@ -242,6 +246,8 @@ import android.util.EventLog;
import android.util.ExceptionUtils;
import android.util.Log;
import android.util.LogPrinter;
+import android.util.LongSparseArray;
+import android.util.LongSparseLongArray;
import android.util.MathUtils;
import android.util.PackageUtils;
import android.util.Pair;
@@ -263,16 +269,13 @@ import com.android.internal.app.ResolverActivity;
import com.android.internal.content.NativeLibraryHelper;
import com.android.internal.content.PackageHelper;
import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.os.IParcelFileDescriptorFactory;
-import com.android.internal.os.RoSystemProperties;
import com.android.internal.os.SomeArgs;
import com.android.internal.os.Zygote;
import com.android.internal.telephony.CarrierAppUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.DumpUtils;
-import com.android.internal.util.FastPrintWriter;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
@@ -292,6 +295,7 @@ import com.android.server.net.NetworkPolicyManagerInternal;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.Settings.DatabaseVersion;
import com.android.server.pm.Settings.VersionInfo;
+import com.android.server.pm.dex.ArtManagerService;
import com.android.server.pm.dex.DexLogger;
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.dex.DexoptOptions;
@@ -307,30 +311,23 @@ import com.android.server.pm.permission.PermissionsState.PermissionState;
import com.android.server.storage.DeviceStorageMonitorInternal;
import dalvik.system.CloseGuard;
-import dalvik.system.DexFile;
import dalvik.system.VMRuntime;
import libcore.io.IoUtils;
-import libcore.io.Streams;
-import libcore.util.EmptyArray;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
-import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -341,15 +338,12 @@ import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
-import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
-import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -363,7 +357,6 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.zip.GZIPInputStream;
/**
* Keep track of all those APKs everywhere.
@@ -467,6 +460,7 @@ public class PackageManagerService extends IPackageManager.Stub
static final int SCAN_AS_SYSTEM = 1<<17;
static final int SCAN_AS_PRIVILEGED = 1<<18;
static final int SCAN_AS_OEM = 1<<19;
+ static final int SCAN_AS_VENDOR = 1<<20;
@IntDef(flag = true, prefix = { "SCAN_" }, value = {
SCAN_NO_DEX,
@@ -617,6 +611,12 @@ public class PackageManagerService extends IPackageManager.Stub
*/
private static final boolean DEFAULT_PACKAGE_PARSER_CACHE_ENABLED = true;
+ /**
+ * Permissions required in order to receive instant application lifecycle broadcasts.
+ */
+ private static final String[] INSTANT_APP_BROADCAST_PERMISSION =
+ new String[] { android.Manifest.permission.ACCESS_INSTANT_APPS };
+
final ServiceThread mHandlerThread;
final PackageHandler mHandler;
@@ -653,18 +653,15 @@ public class PackageManagerService extends IPackageManager.Stub
@GuardedBy("mInstallLock")
final Installer mInstaller;
- /** Directory where installed third-party apps stored */
- final File mAppInstallDir;
-
- /**
- * Directory to which applications installed internally have their
- * 32 bit native libraries copied.
- */
- private File mAppLib32InstallDir;
-
- // Directory containing the private parts (e.g. code and non-resource assets) of forward-locked
- // apps.
- final File mDrmAppPrivateInstallDir;
+ /** Directory where installed applications are stored */
+ private static final File sAppInstallDir =
+ new File(Environment.getDataDirectory(), "app");
+ /** Directory where installed application's 32-bit native libraries are copied. */
+ private static final File sAppLib32InstallDir =
+ new File(Environment.getDataDirectory(), "app-lib");
+ /** Directory where code and non-resource assets of forward-locked applications are stored */
+ private static final File sDrmAppPrivateInstallDir =
+ new File(Environment.getDataDirectory(), "app-private");
// ----------------------------------------------------------------
@@ -745,9 +742,6 @@ public class PackageManagerService extends IPackageManager.Stub
@GuardedBy("mAvailableFeatures")
final ArrayMap<String, FeatureInfo> mAvailableFeatures;
- // If mac_permissions.xml was found for seinfo labeling.
- boolean mFoundPolicyFile;
-
private final InstantAppRegistry mInstantAppRegistry;
@GuardedBy("mPackages")
@@ -765,6 +759,9 @@ public class PackageManagerService extends IPackageManager.Stub
@GuardedBy("mPackages")
final SparseArray<Map<String, Integer>> mChangedPackagesSequenceNumbers = new SparseArray<>();
+ @GuardedBy("mPackages")
+ final private ArraySet<PackageListObserver> mPackageListObservers = new ArraySet<>();
+
class PackageParserCallback implements PackageParser.Callback {
@Override public final boolean hasFeature(String feature) {
return PackageManagerService.this.hasSystemFeature(feature, 0);
@@ -887,8 +884,8 @@ public class PackageManagerService extends IPackageManager.Stub
public final @Nullable String apk;
public final @NonNull SharedLibraryInfo info;
- SharedLibraryEntry(String _path, String _apk, String name, int version, int type,
- String declaringPackageName, int declaringPackageVersionCode) {
+ SharedLibraryEntry(String _path, String _apk, String name, long version, int type,
+ String declaringPackageName, long declaringPackageVersionCode) {
path = _path;
apk = _apk;
info = new SharedLibraryInfo(name, version, type, new VersionedPackage(
@@ -897,8 +894,8 @@ public class PackageManagerService extends IPackageManager.Stub
}
// Currently known shared libraries.
- final ArrayMap<String, SparseArray<SharedLibraryEntry>> mSharedLibraries = new ArrayMap<>();
- final ArrayMap<String, SparseArray<SharedLibraryEntry>> mStaticLibsByDeclaringPackage =
+ final ArrayMap<String, LongSparseArray<SharedLibraryEntry>> mSharedLibraries = new ArrayMap<>();
+ final ArrayMap<String, LongSparseArray<SharedLibraryEntry>> mStaticLibsByDeclaringPackage =
new ArrayMap<>();
// All available activities, for your resolving pleasure.
@@ -938,6 +935,8 @@ public class PackageManagerService extends IPackageManager.Stub
final PackageInstallerService mInstallerService;
+ final ArtManagerService mArtManagerService;
+
private final PackageDexOptimizer mPackageDexOptimizer;
// DexManager handles the usage of dex files (e.g. secondary files, whether or not a package
// is used by other apps).
@@ -1975,16 +1974,20 @@ public class PackageManagerService extends IPackageManager.Stub
// Determine the set of users who are adding this package for
// the first time vs. those who are seeing an update.
- int[] firstUsers = EMPTY_INT_ARRAY;
- int[] updateUsers = EMPTY_INT_ARRAY;
+ int[] firstUserIds = EMPTY_INT_ARRAY;
+ int[] firstInstantUserIds = EMPTY_INT_ARRAY;
+ int[] updateUserIds = EMPTY_INT_ARRAY;
+ int[] instantUserIds = EMPTY_INT_ARRAY;
final boolean allNewUsers = res.origUsers == null || res.origUsers.length == 0;
final PackageSetting ps = (PackageSetting) res.pkg.mExtras;
for (int newUser : res.newUsers) {
- if (ps.getInstantApp(newUser)) {
- continue;
- }
+ final boolean isInstantApp = ps.getInstantApp(newUser);
if (allNewUsers) {
- firstUsers = ArrayUtils.appendInt(firstUsers, newUser);
+ if (isInstantApp) {
+ firstInstantUserIds = ArrayUtils.appendInt(firstInstantUserIds, newUser);
+ } else {
+ firstUserIds = ArrayUtils.appendInt(firstUserIds, newUser);
+ }
continue;
}
boolean isNew = true;
@@ -1995,9 +1998,17 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
if (isNew) {
- firstUsers = ArrayUtils.appendInt(firstUsers, newUser);
+ if (isInstantApp) {
+ firstInstantUserIds = ArrayUtils.appendInt(firstInstantUserIds, newUser);
+ } else {
+ firstUserIds = ArrayUtils.appendInt(firstUserIds, newUser);
+ }
} else {
- updateUsers = ArrayUtils.appendInt(updateUsers, newUser);
+ if (isInstantApp) {
+ instantUserIds = ArrayUtils.appendInt(instantUserIds, newUser);
+ } else {
+ updateUserIds = ArrayUtils.appendInt(updateUserIds, newUser);
+ }
}
}
@@ -2010,7 +2021,7 @@ public class PackageManagerService extends IPackageManager.Stub
int appId = UserHandle.getAppId(res.uid);
boolean isSystem = res.pkg.applicationInfo.isSystemApp();
sendPackageAddedForNewUsers(packageName, isSystem || virtualPreload,
- virtualPreload /*startReceiver*/, appId, firstUsers);
+ virtualPreload /*startReceiver*/, appId, firstUserIds, firstInstantUserIds);
// Send added for users that don't see the package for the first time
Bundle extras = new Bundle(1);
@@ -2020,11 +2031,13 @@ public class PackageManagerService extends IPackageManager.Stub
}
sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
extras, 0 /*flags*/,
- null /*targetPackage*/, null /*finishedReceiver*/, updateUsers);
+ null /*targetPackage*/, null /*finishedReceiver*/,
+ updateUserIds, instantUserIds);
if (installerPackageName != null) {
sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
extras, 0 /*flags*/,
- installerPackageName, null /*finishedReceiver*/, updateUsers);
+ installerPackageName, null /*finishedReceiver*/,
+ updateUserIds, instantUserIds);
}
// Send replaced for users that don't see the package for the first time
@@ -2032,24 +2045,26 @@ public class PackageManagerService extends IPackageManager.Stub
sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED,
packageName, extras, 0 /*flags*/,
null /*targetPackage*/, null /*finishedReceiver*/,
- updateUsers);
+ updateUserIds, instantUserIds);
if (installerPackageName != null) {
sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED, packageName,
extras, 0 /*flags*/,
- installerPackageName, null /*finishedReceiver*/, updateUsers);
+ installerPackageName, null /*finishedReceiver*/,
+ updateUserIds, instantUserIds);
}
sendPackageBroadcast(Intent.ACTION_MY_PACKAGE_REPLACED,
null /*package*/, null /*extras*/, 0 /*flags*/,
packageName /*targetPackage*/,
- null /*finishedReceiver*/, updateUsers);
+ null /*finishedReceiver*/, updateUserIds, instantUserIds);
} else if (launchedForRestore && !isSystemApp(res.pkg)) {
// First-install and we did a restore, so we're responsible for the
// first-launch broadcast.
if (DEBUG_BACKUP) {
Slog.i(TAG, "Post-restore of " + packageName
- + " sending FIRST_LAUNCH in " + Arrays.toString(firstUsers));
+ + " sending FIRST_LAUNCH in " + Arrays.toString(firstUserIds));
}
- sendFirstLaunchBroadcast(packageName, installerPackage, firstUsers);
+ sendFirstLaunchBroadcast(packageName, installerPackage,
+ firstUserIds, firstInstantUserIds);
}
// Send broadcast package appeared if forward locked/external for all users
@@ -2067,9 +2082,9 @@ public class PackageManagerService extends IPackageManager.Stub
}
// Work that needs to happen on first install within each user
- if (firstUsers != null && firstUsers.length > 0) {
+ if (firstUserIds != null && firstUserIds.length > 0) {
synchronized (mPackages) {
- for (int userId : firstUsers) {
+ for (int userId : firstUserIds) {
// If this app is a browser and it's newly-installed for some
// users, clear any default-browser state in those users. The
// app's nature doesn't depend on the user, so we can just check
@@ -2085,6 +2100,10 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
+ if (allNewUsers && !update) {
+ notifyPackageAdded(packageName);
+ }
+
// Log current value of "unknown sources" setting
EventLog.writeEvent(EventLogTags.UNKNOWN_SOURCES_ENABLED,
getUnknownSourcesSettings());
@@ -2107,7 +2126,7 @@ public class PackageManagerService extends IPackageManager.Stub
// should not change.
// Don't notify the manager for ephemeral apps as they are not expected to
// survive long enough to benefit of background optimizations.
- for (int userId : firstUsers) {
+ for (int userId : firstUserIds) {
PackageInfo info = getPackageInfo(packageName, /*flags*/ 0, userId);
// There's a race currently where some install events may interleave with an uninstall.
// This can lead to package info being null (b/36642664).
@@ -2430,11 +2449,6 @@ public class PackageManagerService extends IPackageManager.Stub
Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);
mInstantAppRegistry = new InstantAppRegistry(this);
- File dataDir = Environment.getDataDirectory();
- mAppInstallDir = new File(dataDir, "app");
- mAppLib32InstallDir = new File(dataDir, "app-lib");
- mDrmAppPrivateInstallDir = new File(dataDir, "app-private");
-
ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries();
final int builtInLibCount = libConfig.size();
for (int i = 0; i < builtInLibCount; i++) {
@@ -2444,7 +2458,7 @@ public class PackageManagerService extends IPackageManager.Stub
SharedLibraryInfo.TYPE_BUILTIN, PLATFORM_PACKAGE_NAME, 0);
}
- mFoundPolicyFile = SELinuxMMAC.readInstallPolicy();
+ SELinuxMMAC.readInstallPolicy();
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "read user settings");
mFirstBoot = !mSettings.readLPw(sUserManager.getUsers(false));
@@ -2574,8 +2588,25 @@ public class PackageManagerService extends IPackageManager.Stub
| SCAN_AS_SYSTEM,
0);
- // Collect all vendor packages.
- File vendorAppDir = new File("/vendor/app");
+ // Collected privileged vendor packages.
+ File privilegedVendorAppDir = new File(Environment.getVendorDirectory(),
+ "priv-app");
+ try {
+ privilegedVendorAppDir = privilegedVendorAppDir.getCanonicalFile();
+ } catch (IOException e) {
+ // failed to look up canonical path, continue with original one
+ }
+ scanDirTracedLI(privilegedVendorAppDir,
+ mDefParseFlags
+ | PackageParser.PARSE_IS_SYSTEM_DIR,
+ scanFlags
+ | SCAN_AS_SYSTEM
+ | SCAN_AS_VENDOR
+ | SCAN_AS_PRIVILEGED,
+ 0);
+
+ // Collect ordinary vendor packages.
+ File vendorAppDir = new File(Environment.getVendorDirectory(), "app");
try {
vendorAppDir = vendorAppDir.getCanonicalFile();
} catch (IOException e) {
@@ -2585,7 +2616,8 @@ public class PackageManagerService extends IPackageManager.Stub
mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM_DIR,
scanFlags
- | SCAN_AS_SYSTEM,
+ | SCAN_AS_SYSTEM
+ | SCAN_AS_VENDOR,
0);
// Collect all OEM packages.
@@ -2642,7 +2674,7 @@ public class PackageManagerService extends IPackageManager.Stub
+ ps.name + "; removing system app. Last known codePath="
+ ps.codePathString + ", installStatus=" + ps.installStatus
+ ", versionCode=" + ps.versionCode + "; scanned versionCode="
- + scannedPkg.mVersionCode);
+ + scannedPkg.getLongVersionCode());
removePackageLI(scannedPkg, true);
mExpectingBetter.put(ps.name, ps.codePath);
}
@@ -2704,9 +2736,9 @@ public class PackageManagerService extends IPackageManager.Stub
if (!mOnlyCore) {
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,
SystemClock.uptimeMillis());
- scanDirTracedLI(mAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0);
+ scanDirTracedLI(sAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0);
- scanDirTracedLI(mDrmAppPrivateInstallDir, mDefParseFlags
+ scanDirTracedLI(sDrmAppPrivateInstallDir, mDefParseFlags
| PackageParser.PARSE_FORWARD_LOCK,
scanFlags | SCAN_REQUIRE_KNOWN, 0);
@@ -2770,13 +2802,23 @@ public class PackageManagerService extends IPackageManager.Stub
rescanFlags =
scanFlags
| SCAN_AS_SYSTEM;
+ } else if (FileUtils.contains(privilegedVendorAppDir, scanFile)) {
+ reparseFlags =
+ mDefParseFlags |
+ PackageParser.PARSE_IS_SYSTEM_DIR;
+ rescanFlags =
+ scanFlags
+ | SCAN_AS_SYSTEM
+ | SCAN_AS_VENDOR
+ | SCAN_AS_PRIVILEGED;
} else if (FileUtils.contains(vendorAppDir, scanFile)) {
reparseFlags =
mDefParseFlags |
PackageParser.PARSE_IS_SYSTEM_DIR;
rescanFlags =
scanFlags
- | SCAN_AS_SYSTEM;
+ | SCAN_AS_SYSTEM
+ | SCAN_AS_VENDOR;
} else if (FileUtils.contains(oemAppDir, scanFile)) {
reparseFlags =
mDefParseFlags |
@@ -2865,7 +2907,18 @@ public class PackageManagerService extends IPackageManager.Stub
// NOTE: We ignore potential failures here during a system scan (like
// the rest of the commands above) because there's precious little we
// can do about it. A settings error is reported, though.
- adjustCpuAbisForSharedUserLPw(setting.packages, null /*scannedPackage*/);
+ final List<String> changedAbiCodePath =
+ adjustCpuAbisForSharedUserLPw(setting.packages, null /*scannedPackage*/);
+ if (changedAbiCodePath != null && changedAbiCodePath.size() > 0) {
+ for (int i = changedAbiCodePath.size() - 1; i >= 0; --i) {
+ final String codePathString = changedAbiCodePath.get(i);
+ try {
+ mInstaller.rmdex(codePathString,
+ getDexCodeInstructionSet(getPreferredInstructionSet()));
+ } catch (InstallerException ignored) {
+ }
+ }
+ }
}
// Now that we know all the packages we are keeping,
@@ -3018,6 +3071,7 @@ public class PackageManagerService extends IPackageManager.Stub
}
mInstallerService = new PackageInstallerService(context, this);
+ mArtManagerService = new ArtManagerService(this, mInstaller, mInstallLock);
final Pair<ComponentName, String> instantAppResolverComponent =
getInstantAppResolverLPr();
if (instantAppResolverComponent != null) {
@@ -3830,7 +3884,7 @@ public class PackageManagerService extends IPackageManager.Stub
public PackageInfo getPackageInfoVersioned(VersionedPackage versionedPackage,
int flags, int userId) {
return getPackageInfoInternal(versionedPackage.getPackageName(),
- versionedPackage.getVersionCode(), flags, Binder.getCallingUid(), userId);
+ versionedPackage.getLongVersionCode(), flags, Binder.getCallingUid(), userId);
}
/**
@@ -3839,7 +3893,7 @@ public class PackageManagerService extends IPackageManager.Stub
* to clearing. Because it can only be provided by trusted code, it's value can be
* trusted and will be used as-is; unlike userId which will be validated by this method.
*/
- private PackageInfo getPackageInfoInternal(String packageName, int versionCode,
+ private PackageInfo getPackageInfoInternal(String packageName, long versionCode,
int flags, int filterCallingUid, int userId) {
if (!sUserManager.exists(userId)) return null;
flags = updateFlagsForPackage(flags, userId, packageName);
@@ -4036,7 +4090,7 @@ public class PackageManagerService extends IPackageManager.Stub
if (index < 0) {
continue;
}
- if (uidPs.pkg.usesStaticLibrariesVersions[index] == libEntry.info.getVersion()) {
+ if (uidPs.pkg.usesStaticLibrariesVersions[index] == libEntry.info.getLongVersion()) {
return false;
}
}
@@ -4432,7 +4486,8 @@ public class PackageManagerService extends IPackageManager.Stub
final int[] allUsers = sUserManager.getUserIds();
final int libCount = mSharedLibraries.size();
for (int i = 0; i < libCount; i++) {
- final SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.valueAt(i);
+ final LongSparseArray<SharedLibraryEntry> versionedLib
+ = mSharedLibraries.valueAt(i);
if (versionedLib == null) {
continue;
}
@@ -4449,7 +4504,8 @@ public class PackageManagerService extends IPackageManager.Stub
final VersionedPackage declaringPackage = libInfo.getDeclaringPackage();
// Resolve the package name - we use synthetic package names internally
final String internalPackageName = resolveInternalPackageNameLPr(
- declaringPackage.getPackageName(), declaringPackage.getVersionCode());
+ declaringPackage.getPackageName(),
+ declaringPackage.getLongVersionCode());
final PackageSetting ps = mSettings.getPackageLPr(internalPackageName);
// Skip unused static shared libs cached less than the min period
// to prevent pruning a lib needed by a subsequently installed package.
@@ -4460,7 +4516,7 @@ public class PackageManagerService extends IPackageManager.Stub
packagesToDelete = new ArrayList<>();
}
packagesToDelete.add(new VersionedPackage(internalPackageName,
- declaringPackage.getVersionCode()));
+ declaringPackage.getLongVersionCode()));
}
}
}
@@ -4470,7 +4526,7 @@ public class PackageManagerService extends IPackageManager.Stub
for (int i = 0; i < packageCount; i++) {
final VersionedPackage pkgToDelete = packagesToDelete.get(i);
// Delete the package synchronously (will fail of the lib used for any user).
- if (deletePackageX(pkgToDelete.getPackageName(), pkgToDelete.getVersionCode(),
+ if (deletePackageX(pkgToDelete.getPackageName(), pkgToDelete.getLongVersionCode(),
UserHandle.USER_SYSTEM, PackageManager.DELETE_ALL_USERS)
== PackageManager.DELETE_SUCCEEDED) {
if (volume.getUsableSpace() >= neededSpace) {
@@ -4775,7 +4831,7 @@ public class PackageManagerService extends IPackageManager.Stub
final int libCount = mSharedLibraries.size();
for (int i = 0; i < libCount; i++) {
- SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.valueAt(i);
+ LongSparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.valueAt(i);
if (versionedLib == null) {
continue;
}
@@ -4799,7 +4855,7 @@ public class PackageManagerService extends IPackageManager.Stub
}
SharedLibraryInfo resLibInfo = new SharedLibraryInfo(libInfo.getName(),
- libInfo.getVersion(), libInfo.getType(),
+ libInfo.getLongVersion(), libInfo.getType(),
libInfo.getDeclaringPackage(), getPackagesUsingSharedLibraryLPr(libInfo,
flags, userId));
@@ -4835,7 +4891,7 @@ public class PackageManagerService extends IPackageManager.Stub
if (libIdx < 0) {
continue;
}
- if (ps.usesStaticLibrariesVersions[libIdx] != libInfo.getVersion()) {
+ if (ps.usesStaticLibrariesVersions[libIdx] != libInfo.getLongVersion()) {
continue;
}
if (versionedPackages == null) {
@@ -4916,7 +4972,7 @@ public class PackageManagerService extends IPackageManager.Stub
Set<String> libs = null;
final int libCount = mSharedLibraries.size();
for (int i = 0; i < libCount; i++) {
- SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.valueAt(i);
+ LongSparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.valueAt(i);
if (versionedLib == null) {
continue;
}
@@ -8088,6 +8144,7 @@ public class PackageManagerService extends IPackageManager.Stub
int errorCode = PackageManager.INSTALL_SUCCEEDED;
if (throwable == null) {
+ // TODO(toddke): move lower in the scan chain
// Static shared libraries have synthetic package names
if (parseResult.pkg.applicationInfo.isStaticSharedLibrary()) {
renameStaticSharedLibraryPackage(parseResult.pkg);
@@ -8335,18 +8392,25 @@ public class PackageManagerService extends IPackageManager.Stub
} else {
updatedPs.pkgPrivateFlags &= ~ApplicationInfo.PRIVATE_FLAG_OEM;
}
+ // If new package is not located in "/vendor" (e.g. due to an OTA),
+ // it needs to drop FLAG_VENDOR.
+ if (locationIsVendor(pkg.codePath)) {
+ updatedPs.pkgPrivateFlags |= ApplicationInfo.PRIVATE_FLAG_VENDOR;
+ } else {
+ updatedPs.pkgPrivateFlags &= ~ApplicationInfo.PRIVATE_FLAG_VENDOR;
+ }
if (ps != null && !ps.codePathString.equals(pkg.codePath)) {
// The path has changed from what was last scanned... check the
// version of the new path against what we have stored to determine
// what to do.
if (DEBUG_INSTALL) Slog.d(TAG, "Path changing from " + ps.codePath);
- if (pkg.mVersionCode <= ps.versionCode) {
+ if (pkg.getLongVersionCode() <= ps.versionCode) {
// The system package has been updated and the code path does not match
// Ignore entry. Skip it.
if (DEBUG_INSTALL) Slog.i(TAG, "Package " + ps.name + " at " + pkg.codePath
+ " ignored: updated version " + ps.versionCode
- + " better than this " + pkg.mVersionCode);
+ + " better than this " + pkg.getLongVersionCode());
if (!updatedPs.codePathString.equals(pkg.codePath)) {
Slog.w(PackageManagerService.TAG, "Code path for hidden system pkg "
+ ps.name + " changing from " + updatedPs.codePathString
@@ -8358,7 +8422,7 @@ public class PackageManagerService extends IPackageManager.Stub
updatedPs.resourcePathString = pkg.codePath;
}
updatedPs.pkg = pkg;
- updatedPs.versionCode = pkg.mVersionCode;
+ updatedPs.versionCode = pkg.getLongVersionCode();
// Update the disabled system child packages to point to the package too.
final int childCount = updatedPs.childPackageNames != null
@@ -8369,7 +8433,7 @@ public class PackageManagerService extends IPackageManager.Stub
childPackageName);
if (updatedChildPkg != null) {
updatedChildPkg.pkg = pkg;
- updatedChildPkg.versionCode = pkg.mVersionCode;
+ updatedChildPkg.versionCode = pkg.getLongVersionCode();
}
}
} else {
@@ -8387,7 +8451,7 @@ public class PackageManagerService extends IPackageManager.Stub
logCriticalInfo(Log.WARN, "Package " + ps.name + " at " + pkg.codePath
+ " reverting from " + ps.codePathString
- + ": new version " + pkg.mVersionCode
+ + ": new version " + pkg.getLongVersionCode()
+ " better than installed " + ps.versionCode);
InstallArgs args = createInstallArgsForExisting(packageFlagsToInstallFlags(ps),
@@ -8433,16 +8497,16 @@ public class PackageManagerService extends IPackageManager.Stub
// Set CPU Abis to application info.
if ((scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) != 0) {
final String cpuAbiOverride = deriveAbiOverride(pkg.cpuAbiOverride, updatedPs);
- derivePackageAbi(pkg, cpuAbiOverride, false, mAppLib32InstallDir);
+ derivePackageAbi(pkg, cpuAbiOverride, false);
} else {
pkg.applicationInfo.primaryCpuAbi = updatedPs.primaryCpuAbiString;
pkg.applicationInfo.secondaryCpuAbi = updatedPs.secondaryCpuAbiString;
}
pkg.mExtras = updatedPs;
- throw new PackageManagerException(Log.WARN, "Package " + ps.name + " at "
- + pkg.codePath + " ignored: updated version " + ps.versionCode
- + " better than this " + pkg.mVersionCode);
+ throw new PackageManagerException(Log.WARN, "Package " + pkg.packageName + " at "
+ + pkg.codePath + " ignored: updated version " + updatedPs.versionCode
+ + " better than this " + pkg.getLongVersionCode());
}
if (isUpdatedPkg) {
@@ -8455,11 +8519,17 @@ public class PackageManagerService extends IPackageManager.Stub
scanFlags |= SCAN_AS_PRIVILEGED;
}
- // An updated OEM app will not have the PARSE_IS_OEM
+ // An updated OEM app will not have the SCAN_AS_OEM
// flag set initially
if ((updatedPs.pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0) {
scanFlags |= SCAN_AS_OEM;
}
+
+ // An updated vendor app will not have the SCAN_AS_VENDOR
+ // flag set initially
+ if ((updatedPs.pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0) {
+ scanFlags |= SCAN_AS_VENDOR;
+ }
}
// Verify certificates against what was last scanned
@@ -8491,11 +8561,11 @@ public class PackageManagerService extends IPackageManager.Stub
* already installed version, hide it. It will be scanned later
* and re-added like an update.
*/
- if (pkg.mVersionCode <= ps.versionCode) {
+ if (pkg.getLongVersionCode() <= ps.versionCode) {
shouldHideSystemApp = true;
logCriticalInfo(Log.INFO, "Package " + ps.name + " appeared at " + pkg.codePath
- + " but new version " + pkg.mVersionCode + " better than installed "
- + ps.versionCode + "; hiding system");
+ + " but new version " + pkg.getLongVersionCode()
+ + " better than installed " + ps.versionCode + "; hiding system");
} else {
/*
* The newly found system app is a newer version that the
@@ -8505,7 +8575,8 @@ public class PackageManagerService extends IPackageManager.Stub
*/
logCriticalInfo(Log.WARN, "Package " + ps.name + " at " + pkg.codePath
+ " reverting from " + ps.codePathString + ": new version "
- + pkg.mVersionCode + " better than installed " + ps.versionCode);
+ + pkg.getLongVersionCode() + " better than installed "
+ + ps.versionCode);
InstallArgs args = createInstallArgsForExisting(packageFlagsToInstallFlags(ps),
ps.codePathString, ps.resourcePathString, getAppDexInstructionSets(ps));
synchronized (mInstallLock) {
@@ -8534,7 +8605,7 @@ public class PackageManagerService extends IPackageManager.Stub
}
// Note that we invoke the following method only if we are about to unpack an application
- PackageParser.Package scannedPkg = scanPackageLI(pkg, parseFlags, scanFlags
+ PackageParser.Package scannedPkg = scanPackageNewLI(pkg, parseFlags, scanFlags
| SCAN_UPDATE_SIGNATURE, currentTime, user);
/*
@@ -9083,12 +9154,12 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
- private void findSharedNonSystemLibrariesRecursive(ArrayList<String> libs, int[] versions,
+ private void findSharedNonSystemLibrariesRecursive(ArrayList<String> libs, long[] versions,
ArrayList<PackageParser.Package> collected, Set<String> collectedNames) {
final int libNameCount = libs.size();
for (int i = 0; i < libNameCount; i++) {
String libName = libs.get(i);
- int version = (versions != null && versions.length == libNameCount)
+ long version = (versions != null && versions.length == libNameCount)
? versions[i] : PackageManager.VERSION_CODE_HIGHEST;
PackageParser.Package libPkg = findSharedNonSystemLibrary(libName, version);
if (libPkg != null) {
@@ -9097,7 +9168,7 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
- private PackageParser.Package findSharedNonSystemLibrary(String name, int version) {
+ private PackageParser.Package findSharedNonSystemLibrary(String name, long version) {
synchronized (mPackages) {
SharedLibraryEntry libEntry = getSharedLibraryEntryLPr(name, version);
if (libEntry != null) {
@@ -9107,8 +9178,8 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
- private SharedLibraryEntry getSharedLibraryEntryLPr(String name, int version) {
- SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(name);
+ private SharedLibraryEntry getSharedLibraryEntryLPr(String name, long version) {
+ LongSparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(name);
if (versionedLib == null) {
return null;
}
@@ -9116,15 +9187,15 @@ public class PackageManagerService extends IPackageManager.Stub
}
private SharedLibraryEntry getLatestSharedLibraVersionLPr(PackageParser.Package pkg) {
- SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(
+ LongSparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(
pkg.staticSharedLibName);
if (versionedLib == null) {
return null;
}
- int previousLibVersion = -1;
+ long previousLibVersion = -1;
final int versionCount = versionedLib.size();
for (int i = 0; i < versionCount; i++) {
- final int libVersion = versionedLib.keyAt(i);
+ final long libVersion = versionedLib.keyAt(i);
if (libVersion < pkg.staticSharedLibVersion) {
previousLibVersion = Math.max(previousLibVersion, libVersion);
}
@@ -9408,14 +9479,14 @@ public class PackageManagerService extends IPackageManager.Stub
}
private Set<String> addSharedLibrariesLPw(@NonNull List<String> requestedLibraries,
- @Nullable int[] requiredVersions, @Nullable String[][] requiredCertDigests,
+ @Nullable long[] requiredVersions, @Nullable String[][] requiredCertDigests,
@NonNull String packageName, @Nullable PackageParser.Package changingLib,
boolean required, int targetSdk, @Nullable Set<String> outUsedLibraries)
throws PackageManagerException {
final int libCount = requestedLibraries.size();
for (int i = 0; i < libCount; i++) {
final String libName = requestedLibraries.get(i);
- final int libVersion = requiredVersions != null ? requiredVersions[i]
+ final long libVersion = requiredVersions != null ? requiredVersions[i]
: SharedLibraryInfo.VERSION_UNDEFINED;
final SharedLibraryEntry libEntry = getSharedLibraryEntryLPr(libName, libVersion);
if (libEntry == null) {
@@ -9430,11 +9501,11 @@ public class PackageManagerService extends IPackageManager.Stub
}
} else {
if (requiredVersions != null && requiredCertDigests != null) {
- if (libEntry.info.getVersion() != requiredVersions[i]) {
+ if (libEntry.info.getLongVersion() != requiredVersions[i]) {
throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
"Package " + packageName + " requires unavailable static shared"
+ " library " + libName + " version "
- + libEntry.info.getVersion() + "; failing!");
+ + libEntry.info.getLongVersion() + "; failing!");
}
PackageParser.Package libPkg = mPackages.get(libEntry.apk);
@@ -9458,7 +9529,7 @@ public class PackageManagerService extends IPackageManager.Stub
if (expectedCertDigests.length != libCertDigests.length) {
throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY,
"Package " + packageName + " requires differently signed" +
- " static sDexLoadReporter.java:45.19hared library; failing!");
+ " static shared library; failing!");
}
// Use a predictable order as signature order may vary
@@ -9555,12 +9626,12 @@ public class PackageManagerService extends IPackageManager.Stub
final PackageParser.Package scannedPkg;
try {
// Scan the parent
- scannedPkg = scanPackageLI(pkg, parseFlags, scanFlags, currentTime, user);
+ scannedPkg = scanPackageNewLI(pkg, parseFlags, scanFlags, currentTime, user);
// Scan the children
final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0;
for (int i = 0; i < childCount; i++) {
PackageParser.Package childPkg = pkg.childPackages.get(i);
- scanPackageLI(childPkg, parseFlags,
+ scanPackageNewLI(childPkg, parseFlags,
scanFlags, currentTime, user);
}
} finally {
@@ -9574,89 +9645,423 @@ public class PackageManagerService extends IPackageManager.Stub
return scannedPkg;
}
- private PackageParser.Package scanPackageLI(PackageParser.Package pkg,
+ /** The result of a package scan. */
+ private static class ScanResult {
+ /** Whether or not the package scan was successful */
+ public final boolean success;
+ /**
+ * The final package settings. This may be the same object passed in
+ * the {@link ScanRequest}, but, with modified values.
+ */
+ @Nullable public final PackageSetting pkgSetting;
+ /** ABI code paths that have changed in the package scan */
+ @Nullable public final List<String> changedAbiCodePath;
+ public ScanResult(
+ boolean success,
+ @Nullable PackageSetting pkgSetting,
+ @Nullable List<String> changedAbiCodePath) {
+ this.success = success;
+ this.pkgSetting = pkgSetting;
+ this.changedAbiCodePath = changedAbiCodePath;
+ }
+ }
+
+ /** A package to be scanned */
+ private static class ScanRequest {
+ /** The parsed package */
+ @NonNull public final PackageParser.Package pkg;
+ /** Shared user settings, if the package has a shared user */
+ @Nullable public final SharedUserSetting sharedUserSetting;
+ /**
+ * Package settings of the currently installed version.
+ * <p><em>IMPORTANT:</em> The contents of this object may be modified
+ * during scan.
+ */
+ @Nullable public final PackageSetting pkgSetting;
+ /** A copy of the settings for the currently installed version */
+ @Nullable public final PackageSetting oldPkgSetting;
+ /** Package settings for the disabled version on the /system partition */
+ @Nullable public final PackageSetting disabledPkgSetting;
+ /** Package settings for the installed version under its original package name */
+ @Nullable public final PackageSetting originalPkgSetting;
+ /** The real package name of a renamed application */
+ @Nullable public final String realPkgName;
+ public final @ParseFlags int parseFlags;
+ public final @ScanFlags int scanFlags;
+ /** The user for which the package is being scanned */
+ @Nullable public final UserHandle user;
+ /** Whether or not the platform package is being scanned */
+ public final boolean isPlatformPackage;
+ public ScanRequest(
+ @NonNull PackageParser.Package pkg,
+ @Nullable SharedUserSetting sharedUserSetting,
+ @Nullable PackageSetting pkgSetting,
+ @Nullable PackageSetting disabledPkgSetting,
+ @Nullable PackageSetting originalPkgSetting,
+ @Nullable String realPkgName,
+ @ParseFlags int parseFlags,
+ @ScanFlags int scanFlags,
+ boolean isPlatformPackage,
+ @Nullable UserHandle user) {
+ this.pkg = pkg;
+ this.pkgSetting = pkgSetting;
+ this.sharedUserSetting = sharedUserSetting;
+ this.oldPkgSetting = pkgSetting == null ? null : new PackageSetting(pkgSetting);
+ this.disabledPkgSetting = disabledPkgSetting;
+ this.originalPkgSetting = originalPkgSetting;
+ this.realPkgName = realPkgName;
+ this.parseFlags = parseFlags;
+ this.scanFlags = scanFlags;
+ this.isPlatformPackage = isPlatformPackage;
+ this.user = user;
+ }
+ }
+
+ @GuardedBy("mInstallLock")
+ private PackageParser.Package scanPackageNewLI(@NonNull PackageParser.Package pkg,
final @ParseFlags int parseFlags, final @ScanFlags int scanFlags, long currentTime,
@Nullable UserHandle user) throws PackageManagerException {
- boolean success = false;
- try {
- final PackageParser.Package res = scanPackageDirtyLI(pkg, parseFlags, scanFlags,
- currentTime, user);
- success = true;
- return res;
- } finally {
- if (!success && (scanFlags & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
- // DELETE_DATA_ON_FAILURES is only used by frozen paths
- destroyAppDataLIF(pkg, UserHandle.USER_ALL,
- StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
- destroyAppProfilesLIF(pkg, UserHandle.USER_ALL);
+
+ final String renamedPkgName = mSettings.getRenamedPackageLPr(pkg.mRealPackage);
+ final String realPkgName = getRealPackageName(pkg, renamedPkgName);
+ if (realPkgName != null) {
+ ensurePackageRenamed(pkg, renamedPkgName);
+ }
+ final PackageSetting originalPkgSetting = getOriginalPackageLocked(pkg, renamedPkgName);
+ final PackageSetting pkgSetting = mSettings.getPackageLPr(pkg.packageName);
+ final PackageSetting disabledPkgSetting =
+ mSettings.getDisabledSystemPkgLPr(pkg.packageName);
+
+ if (mTransferedPackages.contains(pkg.packageName)) {
+ Slog.w(TAG, "Package " + pkg.packageName
+ + " was transferred to another, but its .apk remains");
+ }
+
+ synchronized (mPackages) {
+ applyPolicy(pkg, parseFlags, scanFlags);
+ assertPackageIsValid(pkg, parseFlags, scanFlags);
+
+ SharedUserSetting sharedUserSetting = null;
+ if (pkg.mSharedUserId != null) {
+ // SIDE EFFECTS; may potentially allocate a new shared user
+ sharedUserSetting = mSettings.getSharedUserLPw(
+ pkg.mSharedUserId, 0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true /*create*/);
+ if (DEBUG_PACKAGE_SCANNING) {
+ if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
+ Log.d(TAG, "Shared UserID " + pkg.mSharedUserId
+ + " (uid=" + sharedUserSetting.userId + "):"
+ + " packages=" + sharedUserSetting.packages);
+ }
+ }
+
+ boolean scanSucceeded = false;
+ try {
+ final ScanRequest request = new ScanRequest(pkg, sharedUserSetting, pkgSetting,
+ disabledPkgSetting, originalPkgSetting, realPkgName, parseFlags, scanFlags,
+ (pkg == mPlatformPackage), user);
+ final ScanResult result = scanPackageOnlyLI(request, mFactoryTest, currentTime);
+ if (result.success) {
+ commitScanResultsLocked(request, result);
+ }
+ scanSucceeded = true;
+ } finally {
+ if (!scanSucceeded && (scanFlags & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
+ // DELETE_DATA_ON_FAILURES is only used by frozen paths
+ destroyAppDataLIF(pkg, UserHandle.USER_ALL,
+ StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
+ destroyAppProfilesLIF(pkg, UserHandle.USER_ALL);
+ }
}
}
+ return pkg;
}
/**
- * Returns {@code true} if the given file contains code. Otherwise {@code false}.
+ * Commits the package scan and modifies system state.
+ * <p><em>WARNING:</em> The method may throw an excpetion in the middle
+ * of committing the package, leaving the system in an inconsistent state.
+ * This needs to be fixed so, once we get to this point, no errors are
+ * possible and the system is not left in an inconsistent state.
*/
- private static boolean apkHasCode(String fileName) {
- StrictJarFile jarFile = null;
- try {
- jarFile = new StrictJarFile(fileName,
- false /*verify*/, false /*signatureSchemeRollbackProtectionsEnforced*/);
- return jarFile.findEntry("classes.dex") != null;
- } catch (IOException ignore) {
- } finally {
+ @GuardedBy("mPackages")
+ private void commitScanResultsLocked(@NonNull ScanRequest request, @NonNull ScanResult result)
+ throws PackageManagerException {
+ final PackageParser.Package pkg = request.pkg;
+ final @ParseFlags int parseFlags = request.parseFlags;
+ final @ScanFlags int scanFlags = request.scanFlags;
+ final PackageSetting oldPkgSetting = request.oldPkgSetting;
+ final PackageSetting originalPkgSetting = request.originalPkgSetting;
+ final UserHandle user = request.user;
+ final String realPkgName = request.realPkgName;
+ final PackageSetting pkgSetting = result.pkgSetting;
+ final List<String> changedAbiCodePath = result.changedAbiCodePath;
+ final boolean newPkgSettingCreated = (result.pkgSetting != request.pkgSetting);
+
+ if (newPkgSettingCreated) {
+ if (originalPkgSetting != null) {
+ mSettings.addRenamedPackageLPw(pkg.packageName, originalPkgSetting.name);
+ }
+ // THROWS: when we can't allocate a user id. add call to check if there's
+ // enough space to ensure we won't throw; otherwise, don't modify state
+ mSettings.addUserToSettingLPw(pkgSetting);
+
+ if (originalPkgSetting != null && (scanFlags & SCAN_CHECK_ONLY) == 0) {
+ mTransferedPackages.add(originalPkgSetting.name);
+ }
+ }
+ // TODO(toddke): Consider a method specifically for modifying the Package object
+ // post scan; or, moving this stuff out of the Package object since it has nothing
+ // to do with the package on disk.
+ // We need to have this here because addUserToSettingLPw() is sometimes responsible
+ // for creating the application ID. If we did this earlier, we would be saving the
+ // correct ID.
+ pkg.applicationInfo.uid = pkgSetting.appId;
+
+ mSettings.writeUserRestrictionsLPw(pkgSetting, oldPkgSetting);
+
+ if ((scanFlags & SCAN_CHECK_ONLY) == 0 && realPkgName != null) {
+ mTransferedPackages.add(pkg.packageName);
+ }
+
+ // THROWS: when requested libraries that can't be found. it only changes
+ // the state of the passed in pkg object, so, move to the top of the method
+ // and allow it to abort
+ if ((scanFlags & SCAN_BOOTING) == 0
+ && (parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
+ // Check all shared libraries and map to their actual file path.
+ // We only do this here for apps not on a system dir, because those
+ // are the only ones that can fail an install due to this. We
+ // will take care of the system apps by updating all of their
+ // library paths after the scan is done. Also during the initial
+ // scan don't update any libs as we do this wholesale after all
+ // apps are scanned to avoid dependency based scanning.
+ updateSharedLibrariesLPr(pkg, null);
+ }
+
+ // All versions of a static shared library are referenced with the same
+ // package name. Internally, we use a synthetic package name to allow
+ // multiple versions of the same shared library to be installed. So,
+ // we need to generate the synthetic package name of the latest shared
+ // library in order to compare signatures.
+ PackageSetting signatureCheckPs = pkgSetting;
+ if (pkg.applicationInfo.isStaticSharedLibrary()) {
+ SharedLibraryEntry libraryEntry = getLatestSharedLibraVersionLPr(pkg);
+ if (libraryEntry != null) {
+ signatureCheckPs = mSettings.getPackageLPr(libraryEntry.apk);
+ }
+ }
+
+ final KeySetManagerService ksms = mSettings.mKeySetManagerService;
+ if (ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, scanFlags)) {
+ if (ksms.checkUpgradeKeySetLocked(signatureCheckPs, pkg)) {
+ // We just determined the app is signed correctly, so bring
+ // over the latest parsed certs.
+ pkgSetting.signatures.mSignatures = pkg.mSignatures;
+ } else {
+ if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
+ throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
+ "Package " + pkg.packageName + " upgrade keys do not match the "
+ + "previously installed version");
+ } else {
+ pkgSetting.signatures.mSignatures = pkg.mSignatures;
+ String msg = "System package " + pkg.packageName
+ + " signature changed; retaining data.";
+ reportSettingsProblem(Log.WARN, msg);
+ }
+ }
+ } else {
try {
- if (jarFile != null) {
- jarFile.close();
+ final boolean compareCompat = isCompatSignatureUpdateNeeded(pkg);
+ final boolean compareRecover = isRecoverSignatureUpdateNeeded(pkg);
+ final boolean compatMatch = verifySignatures(signatureCheckPs, pkg.mSignatures,
+ compareCompat, compareRecover);
+ // The new KeySets will be re-added later in the scanning process.
+ if (compatMatch) {
+ synchronized (mPackages) {
+ ksms.removeAppKeySetDataLPw(pkg.packageName);
+ }
}
- } catch (IOException ignore) {}
+ // We just determined the app is signed correctly, so bring
+ // over the latest parsed certs.
+ pkgSetting.signatures.mSignatures = pkg.mSignatures;
+ } catch (PackageManagerException e) {
+ if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
+ throw e;
+ }
+ // The signature has changed, but this package is in the system
+ // image... let's recover!
+ pkgSetting.signatures.mSignatures = pkg.mSignatures;
+ // However... if this package is part of a shared user, but it
+ // doesn't match the signature of the shared user, let's fail.
+ // What this means is that you can't change the signatures
+ // associated with an overall shared user, which doesn't seem all
+ // that unreasonable.
+ if (signatureCheckPs.sharedUser != null) {
+ if (compareSignatures(signatureCheckPs.sharedUser.signatures.mSignatures,
+ pkg.mSignatures) != PackageManager.SIGNATURE_MATCH) {
+ throw new PackageManagerException(
+ INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
+ "Signature mismatch for shared user: "
+ + pkgSetting.sharedUser);
+ }
+ }
+ // File a report about this.
+ String msg = "System package " + pkg.packageName
+ + " signature changed; retaining data.";
+ reportSettingsProblem(Log.WARN, msg);
+ }
+ }
+
+ if ((scanFlags & SCAN_CHECK_ONLY) == 0 && pkg.mAdoptPermissions != null) {
+ // This package wants to adopt ownership of permissions from
+ // another package.
+ for (int i = pkg.mAdoptPermissions.size() - 1; i >= 0; i--) {
+ final String origName = pkg.mAdoptPermissions.get(i);
+ final PackageSetting orig = mSettings.getPackageLPr(origName);
+ if (orig != null) {
+ if (verifyPackageUpdateLPr(orig, pkg)) {
+ Slog.i(TAG, "Adopting permissions from " + origName + " to "
+ + pkg.packageName);
+ mSettings.mPermissions.transferPermissions(origName, pkg.packageName);
+ }
+ }
+ }
+ }
+
+ if (changedAbiCodePath != null && changedAbiCodePath.size() > 0) {
+ for (int i = changedAbiCodePath.size() - 1; i <= 0; --i) {
+ final String codePathString = changedAbiCodePath.get(i);
+ try {
+ mInstaller.rmdex(codePathString,
+ getDexCodeInstructionSet(getPreferredInstructionSet()));
+ } catch (InstallerException ignored) {
+ }
+ }
+ }
+
+ if ((scanFlags & SCAN_CHECK_ONLY) != 0) {
+ if (oldPkgSetting != null) {
+ synchronized (mPackages) {
+ mSettings.mPackages.put(oldPkgSetting.name, oldPkgSetting);
+ }
+ }
+ } else {
+ final int userId = user == null ? 0 : user.getIdentifier();
+ // Modify state for the given package setting
+ commitPackageSettings(pkg, pkgSetting, user, scanFlags,
+ (parseFlags & PackageParser.PARSE_CHATTY) != 0 /*chatty*/);
+ if (pkgSetting.getInstantApp(userId)) {
+ mInstantAppRegistry.addInstantAppLPw(userId, pkgSetting.appId);
+ }
}
- return false;
}
/**
- * Enforces code policy for the package. This ensures that if an APK has
- * declared hasCode="true" in its manifest that the APK actually contains
- * code.
- *
- * @throws PackageManagerException If bytecode could not be found when it should exist
+ * Returns the "real" name of the package.
+ * <p>This may differ from the package's actual name if the application has already
+ * been installed under one of this package's original names.
*/
- private static void assertCodePolicy(PackageParser.Package pkg)
- throws PackageManagerException {
- final boolean shouldHaveCode =
- (pkg.applicationInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0;
- if (shouldHaveCode && !apkHasCode(pkg.baseCodePath)) {
- throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
- "Package " + pkg.baseCodePath + " code is missing");
+ private static @Nullable String getRealPackageName(@NonNull PackageParser.Package pkg,
+ @Nullable String renamedPkgName) {
+ if (pkg.mOriginalPackages == null || !pkg.mOriginalPackages.contains(renamedPkgName)) {
+ return null;
}
+ return pkg.mRealPackage;
+ }
- if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) {
- for (int i = 0; i < pkg.splitCodePaths.length; i++) {
- final boolean splitShouldHaveCode =
- (pkg.splitFlags[i] & ApplicationInfo.FLAG_HAS_CODE) != 0;
- if (splitShouldHaveCode && !apkHasCode(pkg.splitCodePaths[i])) {
- throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
- "Package " + pkg.splitCodePaths[i] + " code is missing");
+ /**
+ * Returns the original package setting.
+ * <p>A package can migrate its name during an update. In this scenario, a package
+ * designates a set of names that it considers as one of its original names.
+ * <p>An original package must be signed identically and it must have the same
+ * shared user [if any].
+ */
+ @GuardedBy("mPackages")
+ private @Nullable PackageSetting getOriginalPackageLocked(@NonNull PackageParser.Package pkg,
+ @Nullable String renamedPkgName) {
+ if (pkg.mOriginalPackages == null || pkg.mOriginalPackages.contains(renamedPkgName)) {
+ return null;
+ }
+ for (int i = pkg.mOriginalPackages.size() - 1; i >= 0; --i) {
+ final PackageSetting originalPs =
+ mSettings.getPackageLPr(pkg.mOriginalPackages.get(i));
+ if (originalPs != null) {
+ // the package is already installed under its original name...
+ // but, should we use it?
+ if (!verifyPackageUpdateLPr(originalPs, pkg)) {
+ // the new package is incompatible with the original
+ continue;
+ } else if (originalPs.sharedUser != null) {
+ if (!originalPs.sharedUser.name.equals(pkg.mSharedUserId)) {
+ // the shared user id is incompatible with the original
+ Slog.w(TAG, "Unable to migrate data from " + originalPs.name
+ + " to " + pkg.packageName + ": old uid "
+ + originalPs.sharedUser.name
+ + " differs from " + pkg.mSharedUserId);
+ continue;
+ }
+ // TODO: Add case when shared user id is added [b/28144775]
+ } else {
+ if (DEBUG_UPGRADE) Log.v(TAG, "Renaming new package "
+ + pkg.packageName + " to old name " + originalPs.name);
}
+ return originalPs;
}
}
+ return null;
}
- private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg,
- final @ParseFlags int parseFlags, final @ScanFlags int scanFlags, long currentTime,
- @Nullable UserHandle user)
+ /**
+ * Renames the package if it was installed under a different name.
+ * <p>When we've already installed the package under an original name, update
+ * the new package so we can continue to have the old name.
+ */
+ private static void ensurePackageRenamed(@NonNull PackageParser.Package pkg,
+ @NonNull String renamedPackageName) {
+ if (pkg.mOriginalPackages == null
+ || !pkg.mOriginalPackages.contains(renamedPackageName)
+ || pkg.packageName.equals(renamedPackageName)) {
+ return;
+ }
+ pkg.setPackageName(renamedPackageName);
+ }
+
+ /**
+ * Just scans the package without any side effects.
+ * <p>Not entirely true at the moment. There is still one side effect -- this
+ * method potentially modifies a live {@link PackageSetting} object representing
+ * the package being scanned. This will be resolved in the future.
+ *
+ * @param request Information about the package to be scanned
+ * @param isUnderFactoryTest Whether or not the device is under factory test
+ * @param currentTime The current time, in millis
+ * @return The results of the scan
+ */
+ @GuardedBy("mInstallLock")
+ private static @NonNull ScanResult scanPackageOnlyLI(@NonNull ScanRequest request,
+ boolean isUnderFactoryTest, long currentTime)
throws PackageManagerException {
+ final PackageParser.Package pkg = request.pkg;
+ PackageSetting pkgSetting = request.pkgSetting;
+ final PackageSetting disabledPkgSetting = request.disabledPkgSetting;
+ final PackageSetting originalPkgSetting = request.originalPkgSetting;
+ final @ParseFlags int parseFlags = request.parseFlags;
+ final @ScanFlags int scanFlags = request.scanFlags;
+ final String realPkgName = request.realPkgName;
+ final SharedUserSetting sharedUserSetting = request.sharedUserSetting;
+ final UserHandle user = request.user;
+ final boolean isPlatformPackage = request.isPlatformPackage;
+
+ List<String> changedAbiCodePath = null;
+
if (DEBUG_PACKAGE_SCANNING) {
if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
Log.d(TAG, "Scanning package " + pkg.packageName);
}
- applyPolicy(pkg, parseFlags, scanFlags);
-
- assertPackageIsValid(pkg, parseFlags, scanFlags);
-
if (Build.IS_DEBUGGABLE &&
pkg.isPrivileged() &&
- SystemProperties.getBoolean(PROPERTY_NAME_PM_DEXOPT_PRIV_APPS_OOB, false)) {
+ !SystemProperties.getBoolean("pm.dexopt.priv-apps", true)) {
PackageManagerServiceUtils.logPackageHasUncompressedCode(pkg);
}
@@ -9665,308 +10070,93 @@ public class PackageManagerService extends IPackageManager.Stub
final File destCodeFile = new File(pkg.applicationInfo.getCodePath());
final File destResourceFile = new File(pkg.applicationInfo.getResourcePath());
- SharedUserSetting suid = null;
- PackageSetting pkgSetting = null;
-
- // Getting the package setting may have a side-effect, so if we
- // are only checking if scan would succeed, stash a copy of the
- // old setting to restore at the end.
- PackageSetting nonMutatedPs = null;
-
// We keep references to the derived CPU Abis from settings in oder to reuse
// them in the case where we're not upgrading or booting for the first time.
String primaryCpuAbiFromSettings = null;
String secondaryCpuAbiFromSettings = null;
+ boolean needToDeriveAbi = (scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) != 0;
- // writer
- synchronized (mPackages) {
- if (pkg.mSharedUserId != null) {
- // SIDE EFFECTS; may potentially allocate a new shared user
- suid = mSettings.getSharedUserLPw(
- pkg.mSharedUserId, 0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true /*create*/);
- if (DEBUG_PACKAGE_SCANNING) {
- if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
- Log.d(TAG, "Shared UserID " + pkg.mSharedUserId + " (uid=" + suid.userId
- + "): packages=" + suid.packages);
- }
- }
-
- // Check if we are renaming from an original package name.
- PackageSetting origPackage = null;
- String realName = null;
- if (pkg.mOriginalPackages != null) {
- // This package may need to be renamed to a previously
- // installed name. Let's check on that...
- final String renamed = mSettings.getRenamedPackageLPr(pkg.mRealPackage);
- if (pkg.mOriginalPackages.contains(renamed)) {
- // This package had originally been installed as the
- // original name, and we have already taken care of
- // transitioning to the new one. Just update the new
- // one to continue using the old name.
- realName = pkg.mRealPackage;
- if (!pkg.packageName.equals(renamed)) {
- // Callers into this function may have already taken
- // care of renaming the package; only do it here if
- // it is not already done.
- pkg.setPackageName(renamed);
- }
- } else {
- for (int i=pkg.mOriginalPackages.size()-1; i>=0; i--) {
- if ((origPackage = mSettings.getPackageLPr(
- pkg.mOriginalPackages.get(i))) != null) {
- // We do have the package already installed under its
- // original name... should we use it?
- if (!verifyPackageUpdateLPr(origPackage, pkg)) {
- // New package is not compatible with original.
- origPackage = null;
- continue;
- } else if (origPackage.sharedUser != null) {
- // Make sure uid is compatible between packages.
- if (!origPackage.sharedUser.name.equals(pkg.mSharedUserId)) {
- Slog.w(TAG, "Unable to migrate data from " + origPackage.name
- + " to " + pkg.packageName + ": old uid "
- + origPackage.sharedUser.name
- + " differs from " + pkg.mSharedUserId);
- origPackage = null;
- continue;
- }
- // TODO: Add case when shared user id is added [b/28144775]
- } else {
- if (DEBUG_UPGRADE) Log.v(TAG, "Renaming new package "
- + pkg.packageName + " to old name " + origPackage.name);
- }
- break;
- }
- }
- }
- }
-
- if (mTransferedPackages.contains(pkg.packageName)) {
- Slog.w(TAG, "Package " + pkg.packageName
- + " was transferred to another, but its .apk remains");
- }
-
- // See comments in nonMutatedPs declaration
- if ((scanFlags & SCAN_CHECK_ONLY) != 0) {
- PackageSetting foundPs = mSettings.getPackageLPr(pkg.packageName);
- if (foundPs != null) {
- nonMutatedPs = new PackageSetting(foundPs);
- }
- }
-
- if ((scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) == 0) {
- PackageSetting foundPs = mSettings.getPackageLPr(pkg.packageName);
- if (foundPs != null) {
- primaryCpuAbiFromSettings = foundPs.primaryCpuAbiString;
- secondaryCpuAbiFromSettings = foundPs.secondaryCpuAbiString;
- }
- }
-
- pkgSetting = mSettings.getPackageLPr(pkg.packageName);
- if (pkgSetting != null && pkgSetting.sharedUser != suid) {
- PackageManagerService.reportSettingsProblem(Log.WARN,
- "Package " + pkg.packageName + " shared user changed from "
- + (pkgSetting.sharedUser != null
- ? pkgSetting.sharedUser.name : "<nothing>")
- + " to "
- + (suid != null ? suid.name : "<nothing>")
- + "; replacing with new");
- pkgSetting = null;
- }
- final PackageSetting oldPkgSetting =
- pkgSetting == null ? null : new PackageSetting(pkgSetting);
- final PackageSetting disabledPkgSetting =
- mSettings.getDisabledSystemPkgLPr(pkg.packageName);
-
- String[] usesStaticLibraries = null;
- if (pkg.usesStaticLibraries != null) {
- usesStaticLibraries = new String[pkg.usesStaticLibraries.size()];
- pkg.usesStaticLibraries.toArray(usesStaticLibraries);
- }
-
- if (pkgSetting == null) {
- final String parentPackageName = (pkg.parentPackage != null)
- ? pkg.parentPackage.packageName : null;
- final boolean instantApp = (scanFlags & SCAN_AS_INSTANT_APP) != 0;
- final boolean virtualPreload = (scanFlags & SCAN_AS_VIRTUAL_PRELOAD) != 0;
- // REMOVE SharedUserSetting from method; update in a separate call
- pkgSetting = Settings.createNewSetting(pkg.packageName, origPackage,
- disabledPkgSetting, realName, suid, destCodeFile, destResourceFile,
- pkg.applicationInfo.nativeLibraryRootDir, pkg.applicationInfo.primaryCpuAbi,
- pkg.applicationInfo.secondaryCpuAbi, pkg.mVersionCode,
- pkg.applicationInfo.flags, pkg.applicationInfo.privateFlags, user,
- true /*allowInstall*/, instantApp, virtualPreload,
- parentPackageName, pkg.getChildPackageNames(),
- UserManagerService.getInstance(), usesStaticLibraries,
- pkg.usesStaticLibrariesVersions);
- // SIDE EFFECTS; updates system state; move elsewhere
- if (origPackage != null) {
- mSettings.addRenamedPackageLPw(pkg.packageName, origPackage.name);
- }
- mSettings.addUserToSettingLPw(pkgSetting);
+ if (!needToDeriveAbi) {
+ if (pkgSetting != null) {
+ primaryCpuAbiFromSettings = pkgSetting.primaryCpuAbiString;
+ secondaryCpuAbiFromSettings = pkgSetting.secondaryCpuAbiString;
} else {
- // REMOVE SharedUserSetting from method; update in a separate call.
- //
- // TODO(narayan): This update is bogus. nativeLibraryDir & primaryCpuAbi,
- // secondaryCpuAbi are not known at this point so we always update them
- // to null here, only to reset them at a later point.
- Settings.updatePackageSetting(pkgSetting, disabledPkgSetting, suid, destCodeFile,
- pkg.applicationInfo.nativeLibraryDir, pkg.applicationInfo.primaryCpuAbi,
- pkg.applicationInfo.secondaryCpuAbi, pkg.applicationInfo.flags,
- pkg.applicationInfo.privateFlags, pkg.getChildPackageNames(),
- UserManagerService.getInstance(), usesStaticLibraries,
- pkg.usesStaticLibrariesVersions);
- }
- // SIDE EFFECTS; persists system state to files on disk; move elsewhere
- mSettings.writeUserRestrictionsLPw(pkgSetting, oldPkgSetting);
-
- // SIDE EFFECTS; modifies system state; move elsewhere
- if (pkgSetting.origPackage != null) {
- // If we are first transitioning from an original package,
- // fix up the new package's name now. We need to do this after
- // looking up the package under its new name, so getPackageLP
- // can take care of fiddling things correctly.
- pkg.setPackageName(origPackage.name);
-
- // File a report about this.
- String msg = "New package " + pkgSetting.realName
- + " renamed to replace old package " + pkgSetting.name;
- reportSettingsProblem(Log.WARN, msg);
-
- // Make a note of it.
- if ((scanFlags & SCAN_CHECK_ONLY) == 0) {
- mTransferedPackages.add(origPackage.name);
- }
-
- // No longer need to retain this.
- pkgSetting.origPackage = null;
- }
-
- // SIDE EFFECTS; modifies system state; move elsewhere
- if ((scanFlags & SCAN_CHECK_ONLY) == 0 && realName != null) {
- // Make a note of it.
- mTransferedPackages.add(pkg.packageName);
- }
-
- if (mSettings.isDisabledSystemPackageLPr(pkg.packageName)) {
- pkg.applicationInfo.flags |= ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
- }
-
- if ((scanFlags & SCAN_BOOTING) == 0
- && (parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
- // Check all shared libraries and map to their actual file path.
- // We only do this here for apps not on a system dir, because those
- // are the only ones that can fail an install due to this. We
- // will take care of the system apps by updating all of their
- // library paths after the scan is done. Also during the initial
- // scan don't update any libs as we do this wholesale after all
- // apps are scanned to avoid dependency based scanning.
- updateSharedLibrariesLPr(pkg, null);
- }
-
- if (mFoundPolicyFile) {
- SELinuxMMAC.assignSeInfoValue(pkg);
+ // Re-scanning a system package after uninstalling updates; need to derive ABI
+ needToDeriveAbi = true;
}
- pkg.applicationInfo.uid = pkgSetting.appId;
- pkg.mExtras = pkgSetting;
+ }
+ if (pkgSetting != null && pkgSetting.sharedUser != sharedUserSetting) {
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "Package " + pkg.packageName + " shared user changed from "
+ + (pkgSetting.sharedUser != null
+ ? pkgSetting.sharedUser.name : "<nothing>")
+ + " to "
+ + (sharedUserSetting != null ? sharedUserSetting.name : "<nothing>")
+ + "; replacing with new");
+ pkgSetting = null;
+ }
- // Static shared libs have same package with different versions where
- // we internally use a synthetic package name to allow multiple versions
- // of the same package, therefore we need to compare signatures against
- // the package setting for the latest library version.
- PackageSetting signatureCheckPs = pkgSetting;
- if (pkg.applicationInfo.isStaticSharedLibrary()) {
- SharedLibraryEntry libraryEntry = getLatestSharedLibraVersionLPr(pkg);
- if (libraryEntry != null) {
- signatureCheckPs = mSettings.getPackageLPr(libraryEntry.apk);
- }
- }
+ String[] usesStaticLibraries = null;
+ if (pkg.usesStaticLibraries != null) {
+ usesStaticLibraries = new String[pkg.usesStaticLibraries.size()];
+ pkg.usesStaticLibraries.toArray(usesStaticLibraries);
+ }
+
+ final boolean createNewPackage = (pkgSetting == null);
+ if (createNewPackage) {
+ final String parentPackageName = (pkg.parentPackage != null)
+ ? pkg.parentPackage.packageName : null;
+ final boolean instantApp = (scanFlags & SCAN_AS_INSTANT_APP) != 0;
+ final boolean virtualPreload = (scanFlags & SCAN_AS_VIRTUAL_PRELOAD) != 0;
+ // REMOVE SharedUserSetting from method; update in a separate call
+ pkgSetting = Settings.createNewSetting(pkg.packageName, originalPkgSetting,
+ disabledPkgSetting, realPkgName, sharedUserSetting, destCodeFile,
+ destResourceFile, pkg.applicationInfo.nativeLibraryRootDir,
+ pkg.applicationInfo.primaryCpuAbi, pkg.applicationInfo.secondaryCpuAbi,
+ pkg.mVersionCode, pkg.applicationInfo.flags, pkg.applicationInfo.privateFlags,
+ user, true /*allowInstall*/, instantApp, virtualPreload,
+ parentPackageName, pkg.getChildPackageNames(),
+ UserManagerService.getInstance(), usesStaticLibraries,
+ pkg.usesStaticLibrariesVersions);
+ } else {
+ // REMOVE SharedUserSetting from method; update in a separate call.
+ //
+ // TODO(narayan): This update is bogus. nativeLibraryDir & primaryCpuAbi,
+ // secondaryCpuAbi are not known at this point so we always update them
+ // to null here, only to reset them at a later point.
+ Settings.updatePackageSetting(pkgSetting, disabledPkgSetting, sharedUserSetting,
+ destCodeFile, pkg.applicationInfo.nativeLibraryDir,
+ pkg.applicationInfo.primaryCpuAbi, pkg.applicationInfo.secondaryCpuAbi,
+ pkg.applicationInfo.flags, pkg.applicationInfo.privateFlags,
+ pkg.getChildPackageNames(), UserManagerService.getInstance(),
+ usesStaticLibraries, pkg.usesStaticLibrariesVersions);
+ }
+ if (createNewPackage && originalPkgSetting != null) {
+ // If we are first transitioning from an original package,
+ // fix up the new package's name now. We need to do this after
+ // looking up the package under its new name, so getPackageLP
+ // can take care of fiddling things correctly.
+ pkg.setPackageName(originalPkgSetting.name);
- final KeySetManagerService ksms = mSettings.mKeySetManagerService;
- if (ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, scanFlags)) {
- if (ksms.checkUpgradeKeySetLocked(signatureCheckPs, pkg)) {
- // We just determined the app is signed correctly, so bring
- // over the latest parsed certs.
- pkgSetting.signatures.mSignatures = pkg.mSignatures;
- } else {
- if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
- throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
- "Package " + pkg.packageName + " upgrade keys do not match the "
- + "previously installed version");
- } else {
- pkgSetting.signatures.mSignatures = pkg.mSignatures;
- String msg = "System package " + pkg.packageName
- + " signature changed; retaining data.";
- reportSettingsProblem(Log.WARN, msg);
- }
- }
- } else {
- try {
- final boolean compareCompat = isCompatSignatureUpdateNeeded(pkg);
- final boolean compareRecover = isRecoverSignatureUpdateNeeded(pkg);
- final boolean compatMatch = verifySignatures(signatureCheckPs, pkg.mSignatures,
- compareCompat, compareRecover);
- // The new KeySets will be re-added later in the scanning process.
- if (compatMatch) {
- synchronized (mPackages) {
- ksms.removeAppKeySetDataLPw(pkg.packageName);
- }
- }
- // We just determined the app is signed correctly, so bring
- // over the latest parsed certs.
- pkgSetting.signatures.mSignatures = pkg.mSignatures;
- } catch (PackageManagerException e) {
- if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
- throw e;
- }
- // The signature has changed, but this package is in the system
- // image... let's recover!
- pkgSetting.signatures.mSignatures = pkg.mSignatures;
- // However... if this package is part of a shared user, but it
- // doesn't match the signature of the shared user, let's fail.
- // What this means is that you can't change the signatures
- // associated with an overall shared user, which doesn't seem all
- // that unreasonable.
- if (signatureCheckPs.sharedUser != null) {
- if (compareSignatures(signatureCheckPs.sharedUser.signatures.mSignatures,
- pkg.mSignatures) != PackageManager.SIGNATURE_MATCH) {
- throw new PackageManagerException(
- INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
- "Signature mismatch for shared user: "
- + pkgSetting.sharedUser);
- }
- }
- // File a report about this.
- String msg = "System package " + pkg.packageName
- + " signature changed; retaining data.";
- reportSettingsProblem(Log.WARN, msg);
- }
- }
+ // File a report about this.
+ String msg = "New package " + pkgSetting.realName
+ + " renamed to replace old package " + pkgSetting.name;
+ reportSettingsProblem(Log.WARN, msg);
+ }
- if ((scanFlags & SCAN_CHECK_ONLY) == 0 && pkg.mAdoptPermissions != null) {
- // This package wants to adopt ownership of permissions from
- // another package.
- for (int i = pkg.mAdoptPermissions.size() - 1; i >= 0; i--) {
- final String origName = pkg.mAdoptPermissions.get(i);
- final PackageSetting orig = mSettings.getPackageLPr(origName);
- if (orig != null) {
- if (verifyPackageUpdateLPr(orig, pkg)) {
- Slog.i(TAG, "Adopting permissions from " + origName + " to "
- + pkg.packageName);
- // SIDE EFFECTS; updates permissions system state; move elsewhere
- mSettings.mPermissions.transferPermissions(origName, pkg.packageName);
- }
- }
- }
- }
+ if (disabledPkgSetting != null) {
+ pkg.applicationInfo.flags |= ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
}
+ SELinuxMMAC.assignSeInfoValue(pkg);
+
+ pkg.mExtras = pkgSetting;
pkg.applicationInfo.processName = fixProcessName(
pkg.applicationInfo.packageName,
pkg.applicationInfo.processName);
- if (pkg != mPlatformPackage) {
+ if (!isPlatformPackage) {
// Get all of our default paths setup
pkg.applicationInfo.initForUser(UserHandle.USER_SYSTEM);
}
@@ -9974,10 +10164,10 @@ public class PackageManagerService extends IPackageManager.Stub
final String cpuAbiOverride = deriveAbiOverride(pkg.cpuAbiOverride, pkgSetting);
if ((scanFlags & SCAN_NEW_INSTALL) == 0) {
- if ((scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) != 0) {
+ if (needToDeriveAbi) {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "derivePackageAbi");
final boolean extractNativeLibs = !pkg.isLibrary();
- derivePackageAbi(pkg, cpuAbiOverride, extractNativeLibs, mAppLib32InstallDir);
+ derivePackageAbi(pkg, cpuAbiOverride, extractNativeLibs);
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
// Some system apps still use directory structure for native libraries
@@ -9986,7 +10176,7 @@ public class PackageManagerService extends IPackageManager.Stub
if (isSystemApp(pkg) && !pkg.isUpdatedSystemApp() &&
pkg.applicationInfo.primaryCpuAbi == null) {
setBundledAppAbisAndRoots(pkg, pkgSetting);
- setNativeLibraryPaths(pkg, mAppLib32InstallDir);
+ setNativeLibraryPaths(pkg, sAppLib32InstallDir);
}
} else {
// This is not a first boot or an upgrade, don't bother deriving the
@@ -9995,12 +10185,12 @@ public class PackageManagerService extends IPackageManager.Stub
pkg.applicationInfo.primaryCpuAbi = primaryCpuAbiFromSettings;
pkg.applicationInfo.secondaryCpuAbi = secondaryCpuAbiFromSettings;
- setNativeLibraryPaths(pkg, mAppLib32InstallDir);
+ setNativeLibraryPaths(pkg, sAppLib32InstallDir);
if (DEBUG_ABI_SELECTION) {
Slog.i(TAG, "Using ABIS and native lib paths from settings : " +
- pkg.packageName + " " + pkg.applicationInfo.primaryCpuAbi + ", " +
- pkg.applicationInfo.secondaryCpuAbi);
+ pkg.packageName + " " + pkg.applicationInfo.primaryCpuAbi + ", " +
+ pkg.applicationInfo.secondaryCpuAbi);
}
}
} else {
@@ -10016,14 +10206,14 @@ public class PackageManagerService extends IPackageManager.Stub
// ABIs we've determined above. For non-moves, the path will be updated based on the
// ABIs we determined during compilation, but the path will depend on the final
// package path (after the rename away from the stage path).
- setNativeLibraryPaths(pkg, mAppLib32InstallDir);
+ setNativeLibraryPaths(pkg, sAppLib32InstallDir);
}
// This is a special case for the "system" package, where the ABI is
// dictated by the zygote configuration (and init.rc). We should keep track
// of this ABI so that we can deal with "normal" applications that run under
// the same UID correctly.
- if (mPlatformPackage == pkg) {
+ if (isPlatformPackage) {
pkg.applicationInfo.primaryCpuAbi = VMRuntime.getRuntime().is64Bit() ?
Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0];
}
@@ -10063,7 +10253,6 @@ public class PackageManagerService extends IPackageManager.Stub
" secondary=" + pkg.applicationInfo.secondaryCpuAbi);
}
- // SIDE EFFECTS; removes DEX files from disk; move elsewhere
if ((scanFlags & SCAN_BOOTING) == 0 && pkgSetting.sharedUser != null) {
// We don't do this here during boot because we can do it all
// at once after scanning all existing packages.
@@ -10071,10 +10260,11 @@ public class PackageManagerService extends IPackageManager.Stub
// We also do this *before* we perform dexopt on this package, so that
// we can avoid redundant dexopts, and also to make sure we've got the
// code and package path correct.
- adjustCpuAbisForSharedUserLPw(pkgSetting.sharedUser.packages, pkg);
+ changedAbiCodePath =
+ adjustCpuAbisForSharedUserLPw(pkgSetting.sharedUser.packages, pkg);
}
- if (mFactoryTest && pkg.requestedPermissions.contains(
+ if (isUnderFactoryTest && pkg.requestedPermissions.contains(
android.Manifest.permission.FACTORY_TEST)) {
pkg.applicationInfo.flags |= ApplicationInfo.FLAG_FACTORY_TEST;
}
@@ -10103,22 +10293,55 @@ public class PackageManagerService extends IPackageManager.Stub
}
pkgSetting.setTimeStamp(scanFileTime);
- if ((scanFlags & SCAN_CHECK_ONLY) != 0) {
- if (nonMutatedPs != null) {
- synchronized (mPackages) {
- mSettings.mPackages.put(nonMutatedPs.name, nonMutatedPs);
+ return new ScanResult(true, pkgSetting, changedAbiCodePath);
+ }
+
+ /**
+ * Returns {@code true} if the given file contains code. Otherwise {@code false}.
+ */
+ private static boolean apkHasCode(String fileName) {
+ StrictJarFile jarFile = null;
+ try {
+ jarFile = new StrictJarFile(fileName,
+ false /*verify*/, false /*signatureSchemeRollbackProtectionsEnforced*/);
+ return jarFile.findEntry("classes.dex") != null;
+ } catch (IOException ignore) {
+ } finally {
+ try {
+ if (jarFile != null) {
+ jarFile.close();
+ }
+ } catch (IOException ignore) {}
+ }
+ return false;
+ }
+
+ /**
+ * Enforces code policy for the package. This ensures that if an APK has
+ * declared hasCode="true" in its manifest that the APK actually contains
+ * code.
+ *
+ * @throws PackageManagerException If bytecode could not be found when it should exist
+ */
+ private static void assertCodePolicy(PackageParser.Package pkg)
+ throws PackageManagerException {
+ final boolean shouldHaveCode =
+ (pkg.applicationInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0;
+ if (shouldHaveCode && !apkHasCode(pkg.baseCodePath)) {
+ throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
+ "Package " + pkg.baseCodePath + " code is missing");
+ }
+
+ if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) {
+ for (int i = 0; i < pkg.splitCodePaths.length; i++) {
+ final boolean splitShouldHaveCode =
+ (pkg.splitFlags[i] & ApplicationInfo.FLAG_HAS_CODE) != 0;
+ if (splitShouldHaveCode && !apkHasCode(pkg.splitCodePaths[i])) {
+ throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
+ "Package " + pkg.splitCodePaths[i] + " code is missing");
}
- }
- } else {
- final int userId = user == null ? 0 : user.getIdentifier();
- // Modify state for the given package setting
- commitPackageSettings(pkg, pkgSetting, user, scanFlags,
- (parseFlags & PackageParser.PARSE_CHATTY) != 0 /*chatty*/);
- if (pkgSetting.getInstantApp(userId)) {
- mInstantAppRegistry.addInstantAppLPw(userId, pkgSetting.appId);
}
}
- return pkg;
}
/**
@@ -10128,7 +10351,7 @@ public class PackageManagerService extends IPackageManager.Stub
* Implementation detail: This method must NOT have any side effect. It would
* ideally be static, but, it requires locks to read system state.
*/
- private void applyPolicy(PackageParser.Package pkg, final @ParseFlags int parseFlags,
+ private static void applyPolicy(PackageParser.Package pkg, final @ParseFlags int parseFlags,
final @ScanFlags int scanFlags) {
if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
@@ -10208,6 +10431,10 @@ public class PackageManagerService extends IPackageManager.Stub
pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_OEM;
}
+ if ((scanFlags & SCAN_AS_VENDOR) != 0) {
+ pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_VENDOR;
+ }
+
if (!isSystemApp(pkg)) {
// Only system apps can use these features.
pkg.mOriginalPackages = null;
@@ -10357,20 +10584,20 @@ public class PackageManagerService extends IPackageManager.Stub
}
// The version codes must be ordered as lib versions
- int minVersionCode = Integer.MIN_VALUE;
- int maxVersionCode = Integer.MAX_VALUE;
+ long minVersionCode = Long.MIN_VALUE;
+ long maxVersionCode = Long.MAX_VALUE;
- SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(
+ LongSparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(
pkg.staticSharedLibName);
if (versionedLib != null) {
final int versionCount = versionedLib.size();
for (int i = 0; i < versionCount; i++) {
SharedLibraryInfo libInfo = versionedLib.valueAt(i).info;
- final int libVersionCode = libInfo.getDeclaringPackage()
- .getVersionCode();
- if (libInfo.getVersion() < pkg.staticSharedLibVersion) {
+ final long libVersionCode = libInfo.getDeclaringPackage()
+ .getLongVersionCode();
+ if (libInfo.getLongVersion() < pkg.staticSharedLibVersion) {
minVersionCode = Math.max(minVersionCode, libVersionCode + 1);
- } else if (libInfo.getVersion() > pkg.staticSharedLibVersion) {
+ } else if (libInfo.getLongVersion() > pkg.staticSharedLibVersion) {
maxVersionCode = Math.min(maxVersionCode, libVersionCode - 1);
} else {
minVersionCode = maxVersionCode = libVersionCode;
@@ -10378,7 +10605,8 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
}
- if (pkg.mVersionCode < minVersionCode || pkg.mVersionCode > maxVersionCode) {
+ if (pkg.getLongVersionCode() < minVersionCode
+ || pkg.getLongVersionCode() > maxVersionCode) {
throw new PackageManagerException("Static shared"
+ " lib version codes must be ordered as lib versions");
}
@@ -10468,11 +10696,11 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
- private boolean addSharedLibraryLPw(String path, String apk, String name, int version,
- int type, String declaringPackageName, int declaringVersionCode) {
- SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(name);
+ private boolean addSharedLibraryLPw(String path, String apk, String name, long version,
+ int type, String declaringPackageName, long declaringVersionCode) {
+ LongSparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(name);
if (versionedLib == null) {
- versionedLib = new SparseArray<>();
+ versionedLib = new LongSparseArray<>();
mSharedLibraries.put(name, versionedLib);
if (type == SharedLibraryInfo.TYPE_STATIC) {
mStaticLibsByDeclaringPackage.put(declaringPackageName, versionedLib);
@@ -10486,8 +10714,8 @@ public class PackageManagerService extends IPackageManager.Stub
return true;
}
- private boolean removeSharedLibraryLPw(String name, int version) {
- SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(name);
+ private boolean removeSharedLibraryLPw(String name, long version) {
+ LongSparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(name);
if (versionedLib == null) {
return false;
}
@@ -10512,8 +10740,7 @@ public class PackageManagerService extends IPackageManager.Stub
* be available for query, resolution, etc...
*/
private void commitPackageSettings(PackageParser.Package pkg, PackageSetting pkgSetting,
- UserHandle user, final @ScanFlags int scanFlags, boolean chatty)
- throws PackageManagerException {
+ UserHandle user, final @ScanFlags int scanFlags, boolean chatty) {
final String pkgName = pkg.packageName;
if (mCustomResolverComponentName != null &&
mCustomResolverComponentName.getPackageName().equals(pkg.packageName)) {
@@ -10526,6 +10753,7 @@ public class PackageManagerService extends IPackageManager.Stub
// Set up information for our fall-back user intent resolution activity.
mPlatformPackage = pkg;
pkg.mVersionCode = mSdkVersion;
+ pkg.mVersionCodeMajor = 0;
mAndroidApplication = pkg.applicationInfo;
if (!mResolverReplaced) {
mResolveActivity.applicationInfo = mAndroidApplication;
@@ -10567,7 +10795,7 @@ public class PackageManagerService extends IPackageManager.Stub
// names to allow install of multiple versions, so use name from manifest.
if (addSharedLibraryLPw(null, pkg.packageName, pkg.staticSharedLibName,
pkg.staticSharedLibVersion, SharedLibraryInfo.TYPE_STATIC,
- pkg.manifestPackageName, pkg.mVersionCode)) {
+ pkg.manifestPackageName, pkg.getLongVersionCode())) {
hasStaticSharedLibs = true;
} else {
Slog.w(TAG, "Package " + pkg.packageName + " library "
@@ -10613,7 +10841,7 @@ public class PackageManagerService extends IPackageManager.Stub
if (!addSharedLibraryLPw(null, pkg.packageName, name,
SharedLibraryInfo.VERSION_UNDEFINED,
SharedLibraryInfo.TYPE_DYNAMIC,
- pkg.packageName, pkg.mVersionCode)) {
+ pkg.packageName, pkg.getLongVersionCode())) {
Slog.w(TAG, "Package " + pkg.packageName + " library "
+ name + " already exists; skipping");
}
@@ -10865,11 +11093,11 @@ public class PackageManagerService extends IPackageManager.Stub
* If {@code extractLibs} is true, native libraries are extracted from the app if required.
*/
private static void derivePackageAbi(PackageParser.Package pkg, String cpuAbiOverride,
- boolean extractLibs, File appLib32InstallDir)
+ boolean extractLibs)
throws PackageManagerException {
// Give ourselves some initial paths; we'll come back for another
// pass once we've determined ABI below.
- setNativeLibraryPaths(pkg, appLib32InstallDir);
+ setNativeLibraryPaths(pkg, sAppLib32InstallDir);
// We would never need to extract libs for forward-locked and external packages,
// since the container service will do it for us. We shouldn't attempt to
@@ -11019,7 +11247,7 @@ public class PackageManagerService extends IPackageManager.Stub
// Now that we've calculated the ABIs and determined if it's an internal app,
// we will go ahead and populate the nativeLibraryPath.
- setNativeLibraryPaths(pkg, appLib32InstallDir);
+ setNativeLibraryPaths(pkg, sAppLib32InstallDir);
}
/**
@@ -11035,8 +11263,9 @@ public class PackageManagerService extends IPackageManager.Stub
* NOTE: We currently only match for the primary CPU abi string. Matching the secondary
* adds unnecessary complexity.
*/
- private void adjustCpuAbisForSharedUserLPw(Set<PackageSetting> packagesForUser,
- PackageParser.Package scannedPackage) {
+ private static @Nullable List<String> adjustCpuAbisForSharedUserLPw(
+ Set<PackageSetting> packagesForUser, PackageParser.Package scannedPackage) {
+ List<String> changedAbiCodePath = null;
String requiredInstructionSet = null;
if (scannedPackage != null && scannedPackage.applicationInfo.primaryCpuAbi != null) {
requiredInstructionSet = VMRuntime.getInstructionSet(
@@ -11106,15 +11335,15 @@ public class PackageManagerService extends IPackageManager.Stub
+ (scannedPackage != null ? scannedPackage : "null")
+ ")");
}
- try {
- mInstaller.rmdex(ps.codePathString,
- getDexCodeInstructionSet(getPreferredInstructionSet()));
- } catch (InstallerException ignored) {
+ if (changedAbiCodePath == null) {
+ changedAbiCodePath = new ArrayList<>();
}
+ changedAbiCodePath.add(ps.codePathString);
}
}
}
}
+ return changedAbiCodePath;
}
private void setUpCustomResolverActivity(PackageParser.Package pkg) {
@@ -12339,7 +12568,12 @@ public class PackageManagerService extends IPackageManager.Stub
out.print(' ');
filter.service.printComponentShortName(out);
out.print(" filter ");
- out.println(Integer.toHexString(System.identityHashCode(filter)));
+ out.print(Integer.toHexString(System.identityHashCode(filter)));
+ if (filter.service.info.permission != null) {
+ out.print(" permission "); out.println(filter.service.info.permission);
+ } else {
+ out.println();
+ }
}
@Override
@@ -12730,9 +12964,10 @@ public class PackageManagerService extends IPackageManager.Stub
}
};
+ @Override
public void sendPackageBroadcast(final String action, final String pkg, final Bundle extras,
final int flags, final String targetPkg, final IIntentReceiver finishedReceiver,
- final int[] userIds) {
+ final int[] userIds, int[] instantUserIds) {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -12745,33 +12980,11 @@ public class PackageManagerService extends IPackageManager.Stub
} else {
resolvedUserIds = userIds;
}
- for (int id : resolvedUserIds) {
- final Intent intent = new Intent(action,
- pkg != null ? Uri.fromParts(PACKAGE_SCHEME, pkg, null) : null);
- if (extras != null) {
- intent.putExtras(extras);
- }
- if (targetPkg != null) {
- intent.setPackage(targetPkg);
- }
- // Modify the UID when posting to other users
- int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
- if (uid > 0 && UserHandle.getUserId(uid) != id) {
- uid = UserHandle.getUid(id, UserHandle.getAppId(uid));
- intent.putExtra(Intent.EXTRA_UID, uid);
- }
- intent.putExtra(Intent.EXTRA_USER_HANDLE, id);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | flags);
- if (DEBUG_BROADCASTS) {
- RuntimeException here = new RuntimeException("here");
- here.fillInStackTrace();
- Slog.d(TAG, "Sending to user " + id + ": "
- + intent.toShortString(false, true, false, false)
- + " " + intent.getExtras(), here);
- }
- am.broadcastIntent(null, intent, null, finishedReceiver,
- 0, null, null, null, android.app.AppOpsManager.OP_NONE,
- null, finishedReceiver != null, false, id);
+ doSendBroadcast(am, action, pkg, extras, flags, targetPkg, finishedReceiver,
+ resolvedUserIds, false);
+ if (instantUserIds != null && instantUserIds != EMPTY_INT_ARRAY) {
+ doSendBroadcast(am, action, pkg, extras, flags, targetPkg, finishedReceiver,
+ instantUserIds, true);
}
} catch (RemoteException ex) {
}
@@ -12779,6 +12992,77 @@ public class PackageManagerService extends IPackageManager.Stub
});
}
+ @Override
+ public void notifyPackageAdded(String packageName) {
+ final PackageListObserver[] observers;
+ synchronized (mPackages) {
+ if (mPackageListObservers.size() == 0) {
+ return;
+ }
+ observers = (PackageListObserver[]) mPackageListObservers.toArray();
+ }
+ for (int i = observers.length - 1; i >= 0; --i) {
+ observers[i].onPackageAdded(packageName);
+ }
+ }
+
+ @Override
+ public void notifyPackageRemoved(String packageName) {
+ final PackageListObserver[] observers;
+ synchronized (mPackages) {
+ if (mPackageListObservers.size() == 0) {
+ return;
+ }
+ observers = (PackageListObserver[]) mPackageListObservers.toArray();
+ }
+ for (int i = observers.length - 1; i >= 0; --i) {
+ observers[i].onPackageRemoved(packageName);
+ }
+ }
+
+ /**
+ * Sends a broadcast for the given action.
+ * <p>If {@code isInstantApp} is {@code true}, then the broadcast is protected with
+ * the {@link android.Manifest.permission#ACCESS_INSTANT_APPS} permission. This allows
+ * the system and applications allowed to see instant applications to receive package
+ * lifecycle events for instant applications.
+ */
+ private void doSendBroadcast(IActivityManager am, String action, String pkg, Bundle extras,
+ int flags, String targetPkg, IIntentReceiver finishedReceiver,
+ int[] userIds, boolean isInstantApp)
+ throws RemoteException {
+ for (int id : userIds) {
+ final Intent intent = new Intent(action,
+ pkg != null ? Uri.fromParts(PACKAGE_SCHEME, pkg, null) : null);
+ final String[] requiredPermissions =
+ isInstantApp ? INSTANT_APP_BROADCAST_PERMISSION : null;
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ if (targetPkg != null) {
+ intent.setPackage(targetPkg);
+ }
+ // Modify the UID when posting to other users
+ int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ if (uid > 0 && UserHandle.getUserId(uid) != id) {
+ uid = UserHandle.getUid(id, UserHandle.getAppId(uid));
+ intent.putExtra(Intent.EXTRA_UID, uid);
+ }
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, id);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | flags);
+ if (DEBUG_BROADCASTS) {
+ RuntimeException here = new RuntimeException("here");
+ here.fillInStackTrace();
+ Slog.d(TAG, "Sending to user " + id + ": "
+ + intent.toShortString(false, true, false, false)
+ + " " + intent.getExtras(), here);
+ }
+ am.broadcastIntent(null, intent, null, finishedReceiver,
+ 0, null, null, requiredPermissions, android.app.AppOpsManager.OP_NONE,
+ null, finishedReceiver != null, false, id);
+ }
+ }
+
/**
* Check if the external storage media is available. This is true if there
* is a mounted external storage medium or if the external storage is
@@ -12793,14 +13077,13 @@ public class PackageManagerService extends IPackageManager.Stub
if (getInstantAppPackageName(Binder.getCallingUid()) != null) {
return null;
}
- // writer
- synchronized (mPackages) {
- if (!isExternalMediaAvailable()) {
+ if (!isExternalMediaAvailable()) {
// If the external storage is no longer mounted at this point,
// the caller may not have been able to delete all of this
// packages files and can not delete any more. Bail.
- return null;
- }
+ return null;
+ }
+ synchronized (mPackages) {
final ArrayList<PackageCleanItem> pkgs = mSettings.mPackagesToBeCleaned;
if (lastPackage != null) {
pkgs.remove(lastPackage);
@@ -13028,8 +13311,11 @@ public class PackageManagerService extends IPackageManager.Stub
private void sendPackageAddedForUser(String packageName, PackageSetting pkgSetting,
int userId) {
final boolean isSystem = isSystemApp(pkgSetting) || isUpdatedSystemApp(pkgSetting);
+ final boolean isInstantApp = pkgSetting.getInstantApp(userId);
+ final int[] userIds = isInstantApp ? EMPTY_INT_ARRAY : new int[] { userId };
+ final int[] instantUserIds = isInstantApp ? new int[] { userId } : EMPTY_INT_ARRAY;
sendPackageAddedForNewUsers(packageName, isSystem /*sendBootCompleted*/,
- false /*startReceiver*/, pkgSetting.appId, userId);
+ false /*startReceiver*/, pkgSetting.appId, userIds, instantUserIds);
// Send a session commit broadcast
final PackageInstaller.SessionInfo info = new PackageInstaller.SessionInfo();
@@ -13038,18 +13324,21 @@ public class PackageManagerService extends IPackageManager.Stub
sendSessionCommitBroadcast(info, userId);
}
+ @Override
public void sendPackageAddedForNewUsers(String packageName, boolean sendBootCompleted,
- boolean includeStopped, int appId, int... userIds) {
- if (ArrayUtils.isEmpty(userIds)) {
+ boolean includeStopped, int appId, int[] userIds, int[] instantUserIds) {
+ if (ArrayUtils.isEmpty(userIds) && ArrayUtils.isEmpty(instantUserIds)) {
return;
}
Bundle extras = new Bundle(1);
// Set to UID of the first user, EXTRA_UID is automatically updated in sendPackageBroadcast
- extras.putInt(Intent.EXTRA_UID, UserHandle.getUid(userIds[0], appId));
+ final int uid = UserHandle.getUid(
+ (ArrayUtils.isEmpty(userIds) ? instantUserIds[0] : userIds[0]), appId);
+ extras.putInt(Intent.EXTRA_UID, uid);
sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
- packageName, extras, 0, null, null, userIds);
- if (sendBootCompleted) {
+ packageName, extras, 0, null, null, userIds, instantUserIds);
+ if (sendBootCompleted && !ArrayUtils.isEmpty(userIds)) {
mHandler.post(() -> {
for (int userId : userIds) {
sendBootCompletedBroadcastToSystemApp(
@@ -13193,7 +13482,7 @@ public class PackageManagerService extends IPackageManager.Stub
suspended ? Intent.ACTION_PACKAGES_SUSPENDED
: Intent.ACTION_PACKAGES_UNSUSPENDED,
null, extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, null, null,
- new int[] {userId});
+ new int[] {userId}, null);
}
}
@@ -14081,7 +14370,8 @@ public class PackageManagerService extends IPackageManager.Stub
* the first-launch broadcast will be sent implicitly on that basis in POST_INSTALL
* handling.
*/
- void notifyFirstLaunch(final String pkgName, final String installerPackage, final int userId) {
+ void notifyFirstLaunch(final String packageName, final String installerPackage,
+ final int userId) {
// Serialize this with the rest of the install-process message chain. In the
// restore-at-install case, this Runnable will necessarily run before the
// POST_INSTALL message is processed, so the contents of mRunningInstalls
@@ -14096,12 +14386,12 @@ public class PackageManagerService extends IPackageManager.Stub
if (data.res.returnCode != PackageManager.INSTALL_SUCCEEDED) {
continue;
}
- if (pkgName.equals(data.res.pkg.applicationInfo.packageName)) {
+ if (packageName.equals(data.res.pkg.applicationInfo.packageName)) {
// right package; but is it for the right user?
for (int uIndex = 0; uIndex < data.res.newUsers.length; uIndex++) {
if (userId == data.res.newUsers[uIndex]) {
if (DEBUG_BACKUP) {
- Slog.i(TAG, "Package " + pkgName
+ Slog.i(TAG, "Package " + packageName
+ " being restored so deferring FIRST_LAUNCH");
}
return;
@@ -14111,16 +14401,20 @@ public class PackageManagerService extends IPackageManager.Stub
}
// didn't find it, so not being restored
if (DEBUG_BACKUP) {
- Slog.i(TAG, "Package " + pkgName + " sending normal FIRST_LAUNCH");
+ Slog.i(TAG, "Package " + packageName + " sending normal FIRST_LAUNCH");
}
- sendFirstLaunchBroadcast(pkgName, installerPackage, new int[] {userId});
+ final boolean isInstantApp = isInstantApp(packageName, userId);
+ final int[] userIds = isInstantApp ? EMPTY_INT_ARRAY : new int[] { userId };
+ final int[] instantUserIds = isInstantApp ? new int[] { userId } : EMPTY_INT_ARRAY;
+ sendFirstLaunchBroadcast(packageName, installerPackage, userIds, instantUserIds);
}
});
}
- private void sendFirstLaunchBroadcast(String pkgName, String installerPkg, int[] userIds) {
+ private void sendFirstLaunchBroadcast(String pkgName, String installerPkg,
+ int[] userIds, int[] instantUserIds) {
sendPackageBroadcast(Intent.ACTION_PACKAGE_FIRST_LAUNCH, pkgName, null, 0,
- installerPkg, null, userIds);
+ installerPkg, null, userIds, instantUserIds);
}
private abstract class HandlerParams {
@@ -14608,6 +14902,9 @@ public class PackageManagerService extends IPackageManager.Stub
verification.putExtra(PackageManager.EXTRA_VERIFICATION_VERSION_CODE,
pkgLite.versionCode);
+ verification.putExtra(PackageManager.EXTRA_VERIFICATION_LONG_VERSION_CODE,
+ pkgLite.getLongVersionCode());
+
if (verificationInfo != null) {
if (verificationInfo.originatingUri != null) {
verification.putExtra(Intent.EXTRA_ORIGINATING_URI,
@@ -15493,6 +15790,15 @@ public class PackageManagerService extends IPackageManager.Stub
return;
}
+ // check if the new package supports all of the abis which the old package supports
+ boolean oldPkgSupportMultiArch = oldPackage.applicationInfo.secondaryCpuAbi != null;
+ boolean newPkgSupportMultiArch = pkg.applicationInfo.secondaryCpuAbi != null;
+ if (isSystemApp(oldPackage) && oldPkgSupportMultiArch && !newPkgSupportMultiArch) {
+ res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
+ "Update to package " + pkgName + " doesn't support multi arch");
+ return;
+ }
+
// In case of rollback, remember per-user/profile install state
allUsers = sUserManager.getUserIds();
installedUsers = ps.queryInstalledUsers(allUsers, true);
@@ -15574,18 +15880,22 @@ public class PackageManagerService extends IPackageManager.Stub
boolean sysPkg = (isSystemApp(oldPackage));
if (sysPkg) {
- // Set the system/privileged/oem flags as needed
+ // Set the system/privileged/oem/vendor flags as needed
final boolean privileged =
(oldPackage.applicationInfo.privateFlags
& ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0;
final boolean oem =
(oldPackage.applicationInfo.privateFlags
& ApplicationInfo.PRIVATE_FLAG_OEM) != 0;
+ final boolean vendor =
+ (oldPackage.applicationInfo.privateFlags
+ & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0;
final @ParseFlags int systemParseFlags = parseFlags;
final @ScanFlags int systemScanFlags = scanFlags
| SCAN_AS_SYSTEM
| (privileged ? SCAN_AS_PRIVILEGED : 0)
- | (oem ? SCAN_AS_OEM : 0);
+ | (oem ? SCAN_AS_OEM : 0)
+ | (vendor ? SCAN_AS_VENDOR : 0);
replaceSystemPackageLIF(oldPackage, pkg, systemParseFlags, systemScanFlags,
user, allUsers, installerPackageName, res, installReason);
@@ -16166,6 +16476,16 @@ public class PackageManagerService extends IPackageManager.Stub
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
+ // App targetSdkVersion is below min supported version
+ if (!forceSdk && pkg.applicationInfo.isTargetingDeprecatedSdkVersion()) {
+ Slog.w(TAG, "App " + pkg.packageName + " targets deprecated sdk");
+
+ res.setError(INSTALL_FAILED_NEWER_SDK,
+ "App is targeting deprecated sdk (targetSdkVersion should be at least "
+ + Build.VERSION.MIN_SUPPORTED_TARGET_SDK_INT + ").");
+ return;
+ }
+
// Instant apps must have target SDK >= O and have targetSanboxVersion >= 2
if (instantApp && pkg.applicationInfo.targetSdkVersion <= Build.VERSION_CODES.N_MR1) {
Slog.w(TAG, "Instant app package " + pkg.packageName + " does not target O");
@@ -16303,6 +16623,13 @@ public class PackageManagerService extends IPackageManager.Stub
+ " target SDK " + oldTargetSdk + " does.");
return;
}
+ // Prevent persistent apps from being updated
+ if ((oldPackage.applicationInfo.flags & ApplicationInfo.FLAG_PERSISTENT) != 0) {
+ res.setError(PackageManager.INSTALL_FAILED_INVALID_APK,
+ "Package " + oldPackage.packageName + " is a persistent app. "
+ + "Persistent apps are not updateable.");
+ return;
+ }
// Prevent apps from downgrading their targetSandbox.
final int oldTargetSandbox = oldPackage.applicationInfo.targetSandboxVersion;
final int newTargetSandbox = pkg.applicationInfo.targetSandboxVersion;
@@ -16486,7 +16813,7 @@ public class PackageManagerService extends IPackageManager.Stub
String abiOverride = (TextUtils.isEmpty(pkg.cpuAbiOverride) ?
args.abiOverride : pkg.cpuAbiOverride);
final boolean extractNativeLibs = !pkg.isLibrary();
- derivePackageAbi(pkg, abiOverride, extractNativeLibs, mAppLib32InstallDir);
+ derivePackageAbi(pkg, abiOverride, extractNativeLibs);
} catch (PackageManagerException pme) {
Slog.e(TAG, "Error deriving application ABI", pme);
res.setError(INSTALL_FAILED_INTERNAL_ERROR, "Error deriving application ABI");
@@ -16508,10 +16835,46 @@ public class PackageManagerService extends IPackageManager.Stub
return;
}
- // Verify if we need to dexopt the app.
+ if (!instantApp) {
+ startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);
+ } else {
+ if (DEBUG_DOMAIN_VERIFICATION) {
+ Slog.d(TAG, "Not verifying instant app install for app links: " + pkgName);
+ }
+ }
+
+ try (PackageFreezer freezer = freezePackageForInstall(pkgName, installFlags,
+ "installPackageLI")) {
+ if (replace) {
+ if (pkg.applicationInfo.isStaticSharedLibrary()) {
+ // Static libs have a synthetic package name containing the version
+ // and cannot be updated as an update would get a new package name,
+ // unless this is the exact same version code which is useful for
+ // development.
+ PackageParser.Package existingPkg = mPackages.get(pkg.packageName);
+ if (existingPkg != null &&
+ existingPkg.getLongVersionCode() != pkg.getLongVersionCode()) {
+ res.setError(INSTALL_FAILED_DUPLICATE_PACKAGE, "Packages declaring "
+ + "static-shared libs cannot be updated");
+ return;
+ }
+ }
+ replacePackageLIF(pkg, parseFlags, scanFlags, args.user,
+ installerPackageName, res, args.installReason);
+ } else {
+ installNewPackageLIF(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,
+ args.user, installerPackageName, volumeUuid, res, args.installReason);
+ }
+ }
+
+ // Check whether we need to dexopt the app.
//
- // NOTE: it is *important* to call dexopt after doRename which will sync the
- // package data from PackageParser.Package and its corresponding ApplicationInfo.
+ // NOTE: it is IMPORTANT to call dexopt:
+ // - after doRename which will sync the package data from PackageParser.Package and its
+ // corresponding ApplicationInfo.
+ // - after installNewPackageLIF or replacePackageLIF which will update result with the
+ // uid of the application (pkg.applicationInfo.uid).
+ // This update happens in place!
//
// We only need to dexopt if the package meets ALL of the following conditions:
// 1) it is not forward locked.
@@ -16524,10 +16887,11 @@ public class PackageManagerService extends IPackageManager.Stub
// continuous progress to the useur instead of mysteriously blocking somewhere in the
// middle of running an instant app. The default behaviour can be overridden
// via gservices.
- final boolean performDexopt = !forwardLocked
- && !pkg.applicationInfo.isExternalAsec()
- && (!instantApp || Global.getInt(mContext.getContentResolver(),
- Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0);
+ final boolean performDexopt = (res.returnCode == PackageManager.INSTALL_SUCCEEDED)
+ && !forwardLocked
+ && !pkg.applicationInfo.isExternalAsec()
+ && (!instantApp || Global.getInt(mContext.getContentResolver(),
+ Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0);
if (performDexopt) {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
@@ -16536,13 +16900,13 @@ public class PackageManagerService extends IPackageManager.Stub
//
// Also, don't fail application installs if the dexopt step fails.
DexoptOptions dexoptOptions = new DexoptOptions(pkg.packageName,
- REASON_INSTALL,
- DexoptOptions.DEXOPT_BOOT_COMPLETE);
+ REASON_INSTALL,
+ DexoptOptions.DEXOPT_BOOT_COMPLETE);
mPackageDexOptimizer.performDexOpt(pkg, pkg.usesLibraryFiles,
- null /* instructionSets */,
- getOrCreateCompilerPackageStats(pkg),
- mDexManager.getPackageUseInfoOrDefault(pkg.packageName),
- dexoptOptions);
+ null /* instructionSets */,
+ getOrCreateCompilerPackageStats(pkg),
+ mDexManager.getPackageUseInfoOrDefault(pkg.packageName),
+ dexoptOptions);
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
@@ -16552,37 +16916,6 @@ public class PackageManagerService extends IPackageManager.Stub
// TODO: Layering violation
BackgroundDexOptService.notifyPackageChanged(pkg.packageName);
- if (!instantApp) {
- startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);
- } else {
- if (DEBUG_DOMAIN_VERIFICATION) {
- Slog.d(TAG, "Not verifying instant app install for app links: " + pkgName);
- }
- }
-
- try (PackageFreezer freezer = freezePackageForInstall(pkgName, installFlags,
- "installPackageLI")) {
- if (replace) {
- if (pkg.applicationInfo.isStaticSharedLibrary()) {
- // Static libs have a synthetic package name containing the version
- // and cannot be updated as an update would get a new package name,
- // unless this is the exact same version code which is useful for
- // development.
- PackageParser.Package existingPkg = mPackages.get(pkg.packageName);
- if (existingPkg != null && existingPkg.mVersionCode != pkg.mVersionCode) {
- res.setError(INSTALL_FAILED_DUPLICATE_PACKAGE, "Packages declaring "
- + "static-shared libs cannot be updated");
- return;
- }
- }
- replacePackageLIF(pkg, parseFlags, scanFlags, args.user,
- installerPackageName, res, args.installReason);
- } else {
- installNewPackageLIF(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,
- args.user, installerPackageName, volumeUuid, res, args.installReason);
- }
- }
-
synchronized (mPackages) {
final PackageSetting ps = mSettings.mPackages.get(pkgName);
if (ps != null) {
@@ -16759,6 +17092,10 @@ public class PackageManagerService extends IPackageManager.Stub
return (pkg.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0;
}
+ private static boolean isVendorApp(PackageParser.Package pkg) {
+ return (pkg.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0;
+ }
+
private static boolean hasDomainURLs(PackageParser.Package pkg) {
return (pkg.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS) != 0;
}
@@ -16802,7 +17139,7 @@ public class PackageManagerService extends IPackageManager.Stub
return name.startsWith("vmdl") && name.endsWith(".tmp");
}
};
- for (File file : mDrmAppPrivateInstallDir.listFiles(filter)) {
+ for (File file : sDrmAppPrivateInstallDir.listFiles(filter)) {
file.delete();
}
}
@@ -16823,17 +17160,16 @@ public class PackageManagerService extends IPackageManager.Stub
final boolean canViewInstantApps = canViewInstantApps(callingUid, userId);
Preconditions.checkNotNull(versionedPackage);
Preconditions.checkNotNull(observer);
- Preconditions.checkArgumentInRange(versionedPackage.getVersionCode(),
+ Preconditions.checkArgumentInRange(versionedPackage.getLongVersionCode(),
PackageManager.VERSION_CODE_HIGHEST,
- Integer.MAX_VALUE, "versionCode must be >= -1");
+ Long.MAX_VALUE, "versionCode must be >= -1");
final String packageName = versionedPackage.getPackageName();
- final int versionCode = versionedPackage.getVersionCode();
+ final long versionCode = versionedPackage.getLongVersionCode();
final String internalPackageName;
synchronized (mPackages) {
// Normalize package name to handle renamed packages and static libs
- internalPackageName = resolveInternalPackageNameLPr(versionedPackage.getPackageName(),
- versionedPackage.getVersionCode());
+ internalPackageName = resolveInternalPackageNameLPr(packageName, versionCode);
}
final int uid = Binder.getCallingUid();
@@ -16941,24 +17277,24 @@ public class PackageManagerService extends IPackageManager.Stub
return pkg.packageName;
}
- private String resolveInternalPackageNameLPr(String packageName, int versionCode) {
+ private String resolveInternalPackageNameLPr(String packageName, long versionCode) {
// Handle renamed packages
String normalizedPackageName = mSettings.getRenamedPackageLPr(packageName);
packageName = normalizedPackageName != null ? normalizedPackageName : packageName;
// Is this a static library?
- SparseArray<SharedLibraryEntry> versionedLib =
+ LongSparseArray<SharedLibraryEntry> versionedLib =
mStaticLibsByDeclaringPackage.get(packageName);
if (versionedLib == null || versionedLib.size() <= 0) {
return packageName;
}
// Figure out which lib versions the caller can see
- SparseIntArray versionsCallerCanSee = null;
+ LongSparseLongArray versionsCallerCanSee = null;
final int callingAppId = UserHandle.getAppId(Binder.getCallingUid());
if (callingAppId != Process.SYSTEM_UID && callingAppId != Process.SHELL_UID
&& callingAppId != Process.ROOT_UID) {
- versionsCallerCanSee = new SparseIntArray();
+ versionsCallerCanSee = new LongSparseLongArray();
String libName = versionedLib.valueAt(0).info.getName();
String[] uidPackages = getPackagesForUid(Binder.getCallingUid());
if (uidPackages != null) {
@@ -16966,7 +17302,7 @@ public class PackageManagerService extends IPackageManager.Stub
PackageSetting ps = mSettings.getPackageLPr(uidPackage);
final int libIdx = ArrayUtils.indexOf(ps.usesStaticLibraries, libName);
if (libIdx >= 0) {
- final int libVersion = ps.usesStaticLibrariesVersions[libIdx];
+ final long libVersion = ps.usesStaticLibrariesVersions[libIdx];
versionsCallerCanSee.append(libVersion, libVersion);
}
}
@@ -16984,10 +17320,10 @@ public class PackageManagerService extends IPackageManager.Stub
for (int i = 0; i < versionCount; i++) {
SharedLibraryEntry libEntry = versionedLib.valueAt(i);
if (versionsCallerCanSee != null && versionsCallerCanSee.indexOfKey(
- libEntry.info.getVersion()) < 0) {
+ libEntry.info.getLongVersion()) < 0) {
continue;
}
- final int libVersionCode = libEntry.info.getDeclaringPackage().getVersionCode();
+ final long libVersionCode = libEntry.info.getDeclaringPackage().getLongVersionCode();
if (versionCode != PackageManager.VERSION_CODE_HIGHEST) {
if (libVersionCode == versionCode) {
return libEntry.apk;
@@ -16995,7 +17331,7 @@ public class PackageManagerService extends IPackageManager.Stub
} else if (highestVersion == null) {
highestVersion = libEntry;
} else if (libVersionCode > highestVersion.info
- .getDeclaringPackage().getVersionCode()) {
+ .getDeclaringPackage().getLongVersionCode()) {
highestVersion = libEntry;
}
}
@@ -17124,7 +17460,7 @@ public class PackageManagerService extends IPackageManager.Stub
* persisting settings for later use
* sending a broadcast if necessary
*/
- int deletePackageX(String packageName, int versionCode, int userId, int deleteFlags) {
+ int deletePackageX(String packageName, long versionCode, int userId, int deleteFlags) {
final PackageRemovedInfo info = new PackageRemovedInfo(this);
final boolean res;
@@ -17175,7 +17511,7 @@ public class PackageManagerService extends IPackageManager.Stub
if (!ArrayUtils.isEmpty(libClientPackages)) {
Slog.w(TAG, "Not removing package " + pkg.manifestPackageName
+ " hosting lib " + libEntry.info.getName() + " version "
- + libEntry.info.getVersion() + " used by " + libClientPackages
+ + libEntry.info.getLongVersion() + " used by " + libClientPackages
+ " for user " + currUserId);
return PackageManager.DELETE_FAILED_USED_SHARED_LIBRARY;
}
@@ -17242,6 +17578,7 @@ public class PackageManagerService extends IPackageManager.Stub
int[] origUsers;
int[] removedUsers = null;
int[] broadcastUsers = null;
+ int[] instantUserIds = null;
SparseArray<Integer> installReasons;
boolean isRemovedPackageSystemUpdate = false;
boolean isUpdate;
@@ -17287,7 +17624,7 @@ public class PackageManagerService extends IPackageManager.Stub
PackageInstalledInfo installedInfo = appearedChildPackages.valueAt(i);
packageSender.sendPackageAddedForNewUsers(installedInfo.name,
true /*sendBootCompleted*/, false /*startReceiver*/,
- UserHandle.getAppId(installedInfo.uid), installedInfo.newUsers);
+ UserHandle.getAppId(installedInfo.uid), installedInfo.newUsers, null);
}
}
@@ -17296,18 +17633,18 @@ public class PackageManagerService extends IPackageManager.Stub
extras.putInt(Intent.EXTRA_UID, removedAppId >= 0 ? removedAppId : uid);
extras.putBoolean(Intent.EXTRA_REPLACING, true);
packageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
- removedPackage, extras, 0, null /*targetPackage*/, null, null);
+ removedPackage, extras, 0, null /*targetPackage*/, null, null, null);
packageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED,
- removedPackage, extras, 0, null /*targetPackage*/, null, null);
+ removedPackage, extras, 0, null /*targetPackage*/, null, null, null);
packageSender.sendPackageBroadcast(Intent.ACTION_MY_PACKAGE_REPLACED,
- null, null, 0, removedPackage, null, null);
+ null, null, 0, removedPackage, null, null, null);
if (installerPackageName != null) {
packageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
removedPackage, extras, 0 /*flags*/,
- installerPackageName, null, null);
+ installerPackageName, null, null, null);
packageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED,
removedPackage, extras, 0 /*flags*/,
- installerPackageName, null, null);
+ installerPackageName, null, null, null);
}
}
@@ -17328,23 +17665,25 @@ public class PackageManagerService extends IPackageManager.Stub
extras.putBoolean(Intent.EXTRA_REMOVED_FOR_ALL_USERS, removedForAllUsers);
if (removedPackage != null) {
packageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED,
- removedPackage, extras, 0, null /*targetPackage*/, null, broadcastUsers);
+ removedPackage, extras, 0, null /*targetPackage*/, null,
+ broadcastUsers, instantUserIds);
if (installerPackageName != null) {
packageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_REMOVED,
removedPackage, extras, 0 /*flags*/,
- installerPackageName, null, broadcastUsers);
+ installerPackageName, null, broadcastUsers, instantUserIds);
}
if (dataRemoved && !isRemovedPackageSystemUpdate) {
packageSender.sendPackageBroadcast(Intent.ACTION_PACKAGE_FULLY_REMOVED,
removedPackage, extras,
Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND,
- null, null, broadcastUsers);
+ null, null, broadcastUsers, instantUserIds);
+ packageSender.notifyPackageRemoved(removedPackage);
}
}
if (removedAppId >= 0) {
packageSender.sendPackageBroadcast(Intent.ACTION_UID_REMOVED,
null, extras, Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND,
- null, null, broadcastUsers);
+ null, null, broadcastUsers, instantUserIds);
}
}
@@ -17356,12 +17695,14 @@ public class PackageManagerService extends IPackageManager.Stub
}
broadcastUsers = EMPTY_INT_ARRAY;
+ instantUserIds = EMPTY_INT_ARRAY;
for (int i = userIds.length - 1; i >= 0; --i) {
final int userId = userIds[i];
if (deletedPackageSetting.getInstantApp(userId)) {
- continue;
+ instantUserIds = ArrayUtils.appendInt(instantUserIds, userId);
+ } else {
+ broadcastUsers = ArrayUtils.appendInt(broadcastUsers, userId);
}
- broadcastUsers = ArrayUtils.appendInt(broadcastUsers, userId);
}
}
}
@@ -17493,7 +17834,9 @@ public class PackageManagerService extends IPackageManager.Stub
static boolean locationIsPrivileged(String path) {
try {
final File privilegedAppDir = new File(Environment.getRootDirectory(), "priv-app");
- return path.startsWith(privilegedAppDir.getCanonicalPath());
+ final File privilegedVendorAppDir = new File(Environment.getVendorDirectory(), "priv-app");
+ return path.startsWith(privilegedAppDir.getCanonicalPath())
+ || path.startsWith(privilegedVendorAppDir.getCanonicalPath());
} catch (IOException e) {
Slog.e(TAG, "Unable to access code path " + path);
}
@@ -17509,6 +17852,15 @@ public class PackageManagerService extends IPackageManager.Stub
return false;
}
+ static boolean locationIsVendor(String path) {
+ try {
+ return path.startsWith(Environment.getVendorDirectory().getCanonicalPath());
+ } catch (IOException e) {
+ Slog.e(TAG, "Unable to access code path " + path);
+ }
+ return false;
+ }
+
/*
* Tries to delete system package.
*/
@@ -17630,6 +17982,9 @@ public class PackageManagerService extends IPackageManager.Stub
if (locationIsOem(codePathString)) {
scanFlags |= SCAN_AS_OEM;
}
+ if (locationIsVendor(codePathString)) {
+ scanFlags |= SCAN_AS_VENDOR;
+ }
final File codePath = new File(codePathString);
final PackageParser.Package pkg =
@@ -19893,8 +20248,12 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
// little component state change.
final int flags = !componentNames.contains(packageName)
? Intent.FLAG_RECEIVER_REGISTERED_ONLY : 0;
+ final int userId = UserHandle.getUserId(packageUid);
+ final boolean isInstantApp = isInstantApp(packageName, userId);
+ final int[] userIds = isInstantApp ? EMPTY_INT_ARRAY : new int[] { userId };
+ final int[] instantUserIds = isInstantApp ? new int[] { userId } : EMPTY_INT_ARRAY;
sendPackageBroadcast(Intent.ACTION_PACKAGE_CHANGED, packageName, extras, flags, null, null,
- new int[] {UserHandle.getUserId(packageUid)});
+ userIds, instantUserIds);
}
@Override
@@ -20074,10 +20433,9 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
}
sUserManager.systemReady();
-
// If we upgraded grant all default permissions before kicking off.
for (int userId : grantPermissionsUserIds) {
- mDefaultPermissionPolicy.grantDefaultPermissions(mPackages.values(), userId);
+ mDefaultPermissionPolicy.grantDefaultPermissions(userId);
}
if (grantPermissionsUserIds == EMPTY_INT_ARRAY) {
@@ -20236,6 +20594,7 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
pw.println(" dexopt: dump dexopt state");
pw.println(" compiler-stats: dump compiler statistics");
pw.println(" enabled-overlays: dump list of enabled overlay packages");
+ pw.println(" service-permissions: dump permissions required by services");
pw.println(" <package.name>: info about given package");
return;
} else if ("--checkin".equals(opt)) {
@@ -20371,6 +20730,8 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
dumpState.setDump(DumpState.DUMP_COMPILER_STATS);
} else if ("changes".equals(cmd)) {
dumpState.setDump(DumpState.DUMP_CHANGES);
+ } else if ("service-permissions".equals(cmd)) {
+ dumpState.setDump(DumpState.DUMP_SERVICE_PERMISSIONS);
} else if ("write".equals(cmd)) {
synchronized (mPackages) {
mSettings.writeLPr();
@@ -20445,7 +20806,8 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
final Iterator<String> it = mSharedLibraries.keySet().iterator();
while (it.hasNext()) {
String libName = it.next();
- SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(libName);
+ LongSparseArray<SharedLibraryEntry> versionedLib
+ = mSharedLibraries.get(libName);
if (versionedLib == null) {
continue;
}
@@ -20465,7 +20827,7 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
pw.print(libEntry.info.getName());
if (libEntry.info.isStatic()) {
- pw.print(" version=" + libEntry.info.getVersion());
+ pw.print(" version=" + libEntry.info.getLongVersion());
}
if (!checkin) {
pw.print(" -> ");
@@ -20750,6 +21112,25 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
ipw.decreaseIndent();
}
+ if (!checkin && dumpState.isDumping(DumpState.DUMP_SERVICE_PERMISSIONS)
+ && packageName == null) {
+ if (dumpState.onTitlePrinted()) pw.println();
+ pw.println("Service permissions:");
+
+ final Iterator<ServiceIntentInfo> filterIterator = mServices.filterIterator();
+ while (filterIterator.hasNext()) {
+ final ServiceIntentInfo info = filterIterator.next();
+ final ServiceInfo serviceInfo = info.service.info;
+ final String permission = serviceInfo.permission;
+ if (permission != null) {
+ pw.print(" ");
+ pw.print(serviceInfo.getComponentName().flattenToShortString());
+ pw.print(": ");
+ pw.println(permission);
+ }
+ }
+ }
+
if (!checkin && dumpState.isDumping(DumpState.DUMP_DEXOPT)) {
if (dumpState.onTitlePrinted()) pw.println();
dumpDexoptStateLPr(pw, packageName);
@@ -20834,7 +21215,7 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
final int count = mSharedLibraries.size();
for (int i = 0; i < count; i++) {
final String libName = mSharedLibraries.keyAt(i);
- SparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(libName);
+ LongSparseArray<SharedLibraryEntry> versionedLib = mSharedLibraries.get(libName);
if (versionedLib == null) {
continue;
}
@@ -21012,7 +21393,7 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
String action = mediaStatus ? Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE
: Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE;
- sendPackageBroadcast(action, null, extras, 0, null, finishedReceiver, null);
+ sendPackageBroadcast(action, null, extras, 0, null, finishedReceiver, null, null);
}
}
@@ -22101,8 +22482,8 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
void onNewUserCreated(final int userId) {
+ mDefaultPermissionPolicy.grantDefaultPermissions(userId);
synchronized(mPackages) {
- mDefaultPermissionPolicy.grantDefaultPermissions(mPackages.values(), userId);
// If permission review for legacy apps is required, we represent
// dagerous permissions for such apps as always granted runtime
// permissions to keep per user flag state whether review is needed.
@@ -22191,6 +22572,11 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
return mInstallerService;
}
+ @Override
+ public IArtManager getArtManager() {
+ return mArtManagerService;
+ }
+
private boolean userNeedsBadging(int userId) {
int index = mUserNeedsBadging.indexOfKey(userId);
if (index < 0) {
@@ -22339,11 +22725,11 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
*/
private static void checkDowngrade(PackageParser.Package before, PackageInfoLite after)
throws PackageManagerException {
- if (after.versionCode < before.mVersionCode) {
+ if (after.getLongVersionCode() < before.getLongVersionCode()) {
throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
"Update version code " + after.versionCode + " is older than current "
- + before.mVersionCode);
- } else if (after.versionCode == before.mVersionCode) {
+ + before.getLongVersionCode());
+ } else if (after.getLongVersionCode() == before.getLongVersionCode()) {
if (after.baseRevisionCode < before.baseRevisionCode) {
throw new PackageManagerException(INSTALL_FAILED_VERSION_DOWNGRADE,
"Update base revision code " + after.baseRevisionCode
@@ -22530,12 +22916,12 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
@Override
- public int getVersionCodeForPackage(String packageName) throws RemoteException {
+ public long getVersionCodeForPackage(String packageName) throws RemoteException {
try {
int callingUser = UserHandle.getUserId(Binder.getCallingUid());
PackageInfo pInfo = getPackageInfo(packageName, 0, callingUser);
if (pInfo != null) {
- return pInfo.versionCode;
+ return pInfo.getLongVersionCode();
}
} catch (Exception e) {
}
@@ -22584,6 +22970,29 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
@Override
+ public PackageList getPackageList(PackageListObserver observer) {
+ synchronized (mPackages) {
+ final int N = mPackages.size();
+ final ArrayList<String> list = new ArrayList<>(N);
+ for (int i = 0; i < N; i++) {
+ list.add(mPackages.keyAt(i));
+ }
+ final PackageList packageList = new PackageList(list, observer);
+ if (observer != null) {
+ mPackageListObservers.add(packageList);
+ }
+ return packageList;
+ }
+ }
+
+ @Override
+ public void removePackageListObserver(PackageListObserver observer) {
+ synchronized (mPackages) {
+ mPackageListObservers.remove(observer);
+ }
+ }
+
+ @Override
public PackageParser.Package getDisabledPackage(String packageName) {
synchronized (mPackages) {
final PackageSetting ps = mSettings.getDisabledSystemPkgLPr(packageName);
@@ -23236,9 +23645,15 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
interface PackageSender {
+ /**
+ * @param userIds User IDs where the action occurred on a full application
+ * @param instantUserIds User IDs where the action occurred on an instant application
+ */
void sendPackageBroadcast(final String action, final String pkg,
final Bundle extras, final int flags, final String targetPkg,
- final IIntentReceiver finishedReceiver, final int[] userIds);
+ final IIntentReceiver finishedReceiver, final int[] userIds, int[] instantUserIds);
void sendPackageAddedForNewUsers(String packageName, boolean sendBootCompleted,
- boolean includeStopped, int appId, int... userIds);
+ boolean includeStopped, int appId, int[] userIds, int[] instantUserIds);
+ void notifyPackageAdded(String packageName);
+ void notifyPackageRemoved(String packageName);
}
diff --git a/com/android/server/pm/PackageManagerShellCommand.java b/com/android/server/pm/PackageManagerShellCommand.java
index 44f36d17..2d82c469 100644
--- a/com/android/server/pm/PackageManagerShellCommand.java
+++ b/com/android/server/pm/PackageManagerShellCommand.java
@@ -1245,7 +1245,7 @@ class PackageManagerShellCommand extends ShellCommand {
final PrintWriter pw = getOutPrintWriter();
int flags = 0;
int userId = UserHandle.USER_ALL;
- int versionCode = PackageManager.VERSION_CODE_HIGHEST;
+ long versionCode = PackageManager.VERSION_CODE_HIGHEST;
String opt;
while ((opt = getNextOption()) != null) {
@@ -1257,7 +1257,7 @@ class PackageManagerShellCommand extends ShellCommand {
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
case "--versionCode":
- versionCode = Integer.parseInt(getNextArgRequired());
+ versionCode = Long.parseLong(getNextArgRequired());
break;
default:
pw.println("Error: Unknown option: " + opt);
@@ -1374,7 +1374,7 @@ class PackageManagerShellCommand extends ShellCommand {
}
ClearDataObserver obs = new ClearDataObserver();
- ActivityManager.getService().clearApplicationUserData(pkg, obs, userId);
+ ActivityManager.getService().clearApplicationUserData(pkg, false, obs, userId);
synchronized (obs) {
while (!obs.finished) {
try {
@@ -1538,13 +1538,26 @@ class PackageManagerShellCommand extends ShellCommand {
return 0;
}
+ private boolean isVendorApp(String pkg) {
+ try {
+ final PackageInfo info = mInterface.getPackageInfo(pkg, 0, UserHandle.USER_SYSTEM);
+ return info != null && info.applicationInfo.isVendor();
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
private int runGetPrivappPermissions() {
final String pkg = getNextArg();
if (pkg == null) {
getErrPrintWriter().println("Error: no package specified.");
return 1;
}
- ArraySet<String> privAppPermissions = SystemConfig.getInstance().getPrivAppPermissions(pkg);
+
+ ArraySet<String> privAppPermissions = isVendorApp(pkg) ?
+ SystemConfig.getInstance().getVendorPrivAppPermissions(pkg)
+ : SystemConfig.getInstance().getPrivAppPermissions(pkg);
+
getOutPrintWriter().println(privAppPermissions == null
? "{}" : privAppPermissions.toString());
return 0;
@@ -1556,10 +1569,13 @@ class PackageManagerShellCommand extends ShellCommand {
getErrPrintWriter().println("Error: no package specified.");
return 1;
}
- ArraySet<String> privAppDenyPermissions =
- SystemConfig.getInstance().getPrivAppDenyPermissions(pkg);
- getOutPrintWriter().println(privAppDenyPermissions == null
- ? "{}" : privAppDenyPermissions.toString());
+
+ ArraySet<String> privAppPermissions = isVendorApp(pkg) ?
+ SystemConfig.getInstance().getVendorPrivAppDenyPermissions(pkg)
+ : SystemConfig.getInstance().getPrivAppDenyPermissions(pkg);
+
+ getOutPrintWriter().println(privAppPermissions == null
+ ? "{}" : privAppPermissions.toString());
return 0;
}
@@ -1876,13 +1892,16 @@ class PackageManagerShellCommand extends ShellCommand {
final InstallParams params = new InstallParams();
params.sessionParams = sessionParams;
String opt;
+ boolean replaceExisting = true;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "-l":
sessionParams.installFlags |= PackageManager.INSTALL_FORWARD_LOCK;
break;
- case "-r":
- sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
+ case "-r": // ignore
+ break;
+ case "-R":
+ replaceExisting = false;
break;
case "-i":
params.installerPackageName = getNextArg();
@@ -1967,6 +1986,9 @@ class PackageManagerShellCommand extends ShellCommand {
default:
throw new IllegalArgumentException("Unknown option " + opt);
}
+ if (replaceExisting) {
+ sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
+ }
}
return params;
}
@@ -2435,7 +2457,7 @@ class PackageManagerShellCommand extends ShellCommand {
pw.println(" Install an application. Must provide the apk data to install, either as a");
pw.println(" file path or '-' to read from stdin. Options are:");
pw.println(" -l: forward lock application");
- pw.println(" -r: allow replacement of existing application");
+ pw.println(" -R: disallow replacement of existing application");
pw.println(" -t: allow test packages");
pw.println(" -i: specify package name of installer owning the app");
pw.println(" -s: install application on sdcard");
diff --git a/com/android/server/pm/PackageSetting.java b/com/android/server/pm/PackageSetting.java
index 3b414e9a..2b91b7d3 100644
--- a/com/android/server/pm/PackageSetting.java
+++ b/com/android/server/pm/PackageSetting.java
@@ -50,9 +50,9 @@ public final class PackageSetting extends PackageSettingBase {
PackageSetting(String name, String realName, File codePath, File resourcePath,
String legacyNativeLibraryPathString, String primaryCpuAbiString,
String secondaryCpuAbiString, String cpuAbiOverrideString,
- int pVersionCode, int pkgFlags, int privateFlags, String parentPackageName,
+ long pVersionCode, int pkgFlags, int privateFlags, String parentPackageName,
List<String> childPackageNames, int sharedUserId, String[] usesStaticLibraries,
- int[] usesStaticLibrariesVersions) {
+ long[] usesStaticLibrariesVersions) {
super(name, realName, codePath, resourcePath, legacyNativeLibraryPathString,
primaryCpuAbiString, secondaryCpuAbiString, cpuAbiOverrideString,
pVersionCode, pkgFlags, privateFlags, parentPackageName, childPackageNames,
@@ -140,6 +140,10 @@ public final class PackageSetting extends PackageSettingBase {
return (pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0;
}
+ public boolean isVendor() {
+ return (pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0;
+ }
+
public boolean isForwardLocked() {
return (pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_FORWARD_LOCK) != 0;
}
diff --git a/com/android/server/pm/PackageSettingBase.java b/com/android/server/pm/PackageSettingBase.java
index a8387684..809e16cb 100644
--- a/com/android/server/pm/PackageSettingBase.java
+++ b/com/android/server/pm/PackageSettingBase.java
@@ -75,7 +75,7 @@ public abstract class PackageSettingBase extends SettingBase {
String resourcePathString;
String[] usesStaticLibraries;
- int[] usesStaticLibrariesVersions;
+ long[] usesStaticLibrariesVersions;
/**
* The path under which native libraries have been unpacked. This path is
@@ -105,7 +105,7 @@ public abstract class PackageSettingBase extends SettingBase {
long timeStamp;
long firstInstallTime;
long lastUpdateTime;
- int versionCode;
+ long versionCode;
boolean uidError;
@@ -131,7 +131,6 @@ public abstract class PackageSettingBase extends SettingBase {
* using the full set of code paths when the package's process is started.
*/
Set<String> oldCodePaths;
- PackageSettingBase origPackage;
/** Package name of the app that installed this package */
String installerPackageName;
@@ -149,9 +148,9 @@ public abstract class PackageSettingBase extends SettingBase {
PackageSettingBase(String name, String realName, File codePath, File resourcePath,
String legacyNativeLibraryPathString, String primaryCpuAbiString,
String secondaryCpuAbiString, String cpuAbiOverrideString,
- int pVersionCode, int pkgFlags, int pkgPrivateFlags,
+ long pVersionCode, int pkgFlags, int pkgPrivateFlags,
String parentPackageName, List<String> childPackageNames,
- String[] usesStaticLibraries, int[] usesStaticLibrariesVersions) {
+ String[] usesStaticLibraries, long[] usesStaticLibrariesVersions) {
super(pkgFlags, pkgPrivateFlags);
this.name = name;
this.realName = realName;
@@ -180,7 +179,7 @@ public abstract class PackageSettingBase extends SettingBase {
void init(File codePath, File resourcePath, String legacyNativeLibraryPathString,
String primaryCpuAbiString, String secondaryCpuAbiString,
- String cpuAbiOverrideString, int pVersionCode) {
+ String cpuAbiOverrideString, long pVersionCode) {
this.codePath = codePath;
this.codePathString = codePath.toString();
this.resourcePath = resourcePath;
@@ -263,7 +262,6 @@ public abstract class PackageSettingBase extends SettingBase {
lastUpdateTime = orig.lastUpdateTime;
legacyNativeLibraryPathString = orig.legacyNativeLibraryPathString;
// Intentionally skip oldCodePaths; it's not relevant for copies
- origPackage = orig.origPackage;
parentPackageName = orig.parentPackageName;
primaryCpuAbiString = orig.primaryCpuAbiString;
resourcePath = orig.resourcePath;
diff --git a/com/android/server/pm/SELinuxMMAC.java b/com/android/server/pm/SELinuxMMAC.java
index f0ce3c9d..fbf3d824 100644
--- a/com/android/server/pm/SELinuxMMAC.java
+++ b/com/android/server/pm/SELinuxMMAC.java
@@ -59,6 +59,8 @@ public final class SELinuxMMAC {
// All policy stanzas read from mac_permissions.xml. This is also the lock
// to synchronize access during policy load and access attempts.
private static List<Policy> sPolicies = new ArrayList<>();
+ /** Whether or not the policy files have been read */
+ private static boolean sPolicyRead;
/** Path to MAC permissions on system image */
private static final File[] MAC_PERMISSIONS =
@@ -88,6 +90,12 @@ public final class SELinuxMMAC {
* were loaded successfully; no partial loading is possible.
*/
public static boolean readInstallPolicy() {
+ synchronized (sPolicies) {
+ if (sPolicyRead) {
+ return true;
+ }
+ }
+
// Temp structure to hold the rules while we parse the xml file
List<Policy> policies = new ArrayList<>();
@@ -142,7 +150,9 @@ public final class SELinuxMMAC {
}
synchronized (sPolicies) {
- sPolicies = policies;
+ sPolicies.clear();
+ sPolicies.addAll(policies);
+ sPolicyRead = true;
if (DEBUG_POLICY_ORDER) {
for (Policy policy : sPolicies) {
@@ -280,6 +290,12 @@ public final class SELinuxMMAC {
*/
public static void assignSeInfoValue(PackageParser.Package pkg) {
synchronized (sPolicies) {
+ if (!sPolicyRead) {
+ if (DEBUG_POLICY) {
+ Slog.d(TAG, "Policy not read");
+ }
+ return;
+ }
for (Policy policy : sPolicies) {
String seInfo = policy.getMatchedSeInfo(pkg);
if (seInfo != null) {
diff --git a/com/android/server/pm/SettingBase.java b/com/android/server/pm/SettingBase.java
index c97f5e54..46ba0060 100644
--- a/com/android/server/pm/SettingBase.java
+++ b/com/android/server/pm/SettingBase.java
@@ -61,6 +61,7 @@ abstract class SettingBase {
this.pkgPrivateFlags = pkgPrivateFlags
& (ApplicationInfo.PRIVATE_FLAG_PRIVILEGED
| ApplicationInfo.PRIVATE_FLAG_OEM
+ | ApplicationInfo.PRIVATE_FLAG_VENDOR
| ApplicationInfo.PRIVATE_FLAG_FORWARD_LOCK
| ApplicationInfo.PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER);
}
diff --git a/com/android/server/pm/Settings.java b/com/android/server/pm/Settings.java
index ddad6774..648f847a 100644
--- a/com/android/server/pm/Settings.java
+++ b/com/android/server/pm/Settings.java
@@ -593,10 +593,10 @@ public final class Settings {
PackageSetting addPackageLPw(String name, String realName, File codePath, File resourcePath,
String legacyNativeLibraryPathString, String primaryCpuAbiString,
- String secondaryCpuAbiString, String cpuAbiOverrideString, int uid, int vc, int
+ String secondaryCpuAbiString, String cpuAbiOverrideString, int uid, long vc, int
pkgFlags, int pkgPrivateFlags, String parentPackageName,
List<String> childPackageNames, String[] usesStaticLibraries,
- int[] usesStaticLibraryNames) {
+ long[] usesStaticLibraryNames) {
PackageSetting p = mPackages.get(name);
if (p != null) {
if (p.appId == uid) {
@@ -673,11 +673,11 @@ public final class Settings {
static @NonNull PackageSetting createNewSetting(String pkgName, PackageSetting originalPkg,
PackageSetting disabledPkg, String realPkgName, SharedUserSetting sharedUser,
File codePath, File resourcePath, String legacyNativeLibraryPath, String primaryCpuAbi,
- String secondaryCpuAbi, int versionCode, int pkgFlags, int pkgPrivateFlags,
+ String secondaryCpuAbi, long versionCode, int pkgFlags, int pkgPrivateFlags,
UserHandle installUser, boolean allowInstall, boolean instantApp,
boolean virtualPreload, String parentPkgName, List<String> childPkgNames,
UserManagerService userManager,
- String[] usesStaticLibraries, int[] usesStaticLibrariesVersions) {
+ String[] usesStaticLibraries, long[] usesStaticLibrariesVersions) {
final PackageSetting pkgSetting;
if (originalPkg != null) {
if (PackageManagerService.DEBUG_UPGRADE) Log.v(PackageManagerService.TAG, "Package "
@@ -687,7 +687,6 @@ public final class Settings {
(childPkgNames != null) ? new ArrayList<>(childPkgNames) : null;
pkgSetting.codePath = codePath;
pkgSetting.legacyNativeLibraryPathString = legacyNativeLibraryPath;
- pkgSetting.origPackage = originalPkg;
pkgSetting.parentPackageName = parentPkgName;
pkgSetting.pkgFlags = pkgFlags;
pkgSetting.pkgPrivateFlags = pkgPrivateFlags;
@@ -788,7 +787,7 @@ public final class Settings {
@Nullable String primaryCpuAbi, @Nullable String secondaryCpuAbi,
int pkgFlags, int pkgPrivateFlags, @Nullable List<String> childPkgNames,
@NonNull UserManagerService userManager, @Nullable String[] usesStaticLibraries,
- @Nullable int[] usesStaticLibrariesVersions) throws PackageManagerException {
+ @Nullable long[] usesStaticLibrariesVersions) throws PackageManagerException {
final String pkgName = pkgSetting.name;
if (pkgSetting.sharedUser != sharedUser) {
PackageManagerService.reportSettingsProblem(Log.WARN,
@@ -847,6 +846,8 @@ public final class Settings {
pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
pkgSetting.pkgPrivateFlags |=
pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_OEM;
+ pkgSetting.pkgPrivateFlags |=
+ pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR;
pkgSetting.primaryCpuAbiString = primaryCpuAbi;
pkgSetting.secondaryCpuAbiString = secondaryCpuAbi;
if (childPkgNames != null) {
@@ -949,8 +950,8 @@ public final class Settings {
p.secondaryCpuAbiString = pkg.applicationInfo.secondaryCpuAbi;
p.cpuAbiOverrideString = pkg.cpuAbiOverride;
// Update version code if needed
- if (pkg.mVersionCode != p.versionCode) {
- p.versionCode = pkg.mVersionCode;
+ if (pkg.getLongVersionCode() != p.versionCode) {
+ p.versionCode = pkg.getLongVersionCode();
}
// Update signatures if needed.
if (p.signatures.mSignatures == null) {
@@ -2287,9 +2288,9 @@ public final class Settings {
String libName = parser.getAttributeValue(null, ATTR_NAME);
String libVersionStr = parser.getAttributeValue(null, ATTR_VERSION);
- int libVersion = -1;
+ long libVersion = -1;
try {
- libVersion = Integer.parseInt(libVersionStr);
+ libVersion = Long.parseLong(libVersionStr);
} catch (NumberFormatException e) {
// ignore
}
@@ -2297,7 +2298,7 @@ public final class Settings {
if (libName != null && libVersion >= 0) {
outPs.usesStaticLibraries = ArrayUtils.appendElement(String.class,
outPs.usesStaticLibraries, libName);
- outPs.usesStaticLibrariesVersions = ArrayUtils.appendInt(
+ outPs.usesStaticLibrariesVersions = ArrayUtils.appendLong(
outPs.usesStaticLibrariesVersions, libVersion);
}
@@ -2306,7 +2307,7 @@ public final class Settings {
}
void writeUsesStaticLibLPw(XmlSerializer serializer, String[] usesStaticLibraries,
- int[] usesStaticLibraryVersions) throws IOException {
+ long[] usesStaticLibraryVersions) throws IOException {
if (ArrayUtils.isEmpty(usesStaticLibraries) || ArrayUtils.isEmpty(usesStaticLibraryVersions)
|| usesStaticLibraries.length != usesStaticLibraryVersions.length) {
return;
@@ -2314,10 +2315,10 @@ public final class Settings {
final int libCount = usesStaticLibraries.length;
for (int i = 0; i < libCount; i++) {
final String libName = usesStaticLibraries[i];
- final int libVersion = usesStaticLibraryVersions[i];
+ final long libVersion = usesStaticLibraryVersions[i];
serializer.startTag(null, TAG_USES_STATIC_LIB);
serializer.attribute(null, ATTR_NAME, libName);
- serializer.attribute(null, ATTR_VERSION, Integer.toString(libVersion));
+ serializer.attribute(null, ATTR_VERSION, Long.toString(libVersion));
serializer.endTag(null, TAG_USES_STATIC_LIB);
}
}
@@ -3561,10 +3562,10 @@ public final class Settings {
resourcePathStr = codePathStr;
}
String version = parser.getAttributeValue(null, "version");
- int versionCode = 0;
+ long versionCode = 0;
if (version != null) {
try {
- versionCode = Integer.parseInt(version);
+ versionCode = Long.parseLong(version);
} catch (NumberFormatException e) {
}
}
@@ -3677,7 +3678,7 @@ public final class Settings {
long lastUpdateTime = 0;
PackageSetting packageSetting = null;
String version = null;
- int versionCode = 0;
+ long versionCode = 0;
String parentPackageName;
try {
name = parser.getAttributeValue(null, ATTR_NAME);
@@ -3705,7 +3706,7 @@ public final class Settings {
version = parser.getAttributeValue(null, "version");
if (version != null) {
try {
- versionCode = Integer.parseInt(version);
+ versionCode = Long.parseLong(version);
} catch (NumberFormatException e) {
}
}
@@ -4421,6 +4422,7 @@ public final class Settings {
ApplicationInfo.PRIVATE_FLAG_PRIVILEGED, "PRIVILEGED",
ApplicationInfo.PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER, "REQUIRED_FOR_SYSTEM_USER",
ApplicationInfo.PRIVATE_FLAG_STATIC_SHARED_LIBRARY, "STATIC_SHARED_LIBRARY",
+ ApplicationInfo.PRIVATE_FLAG_VENDOR, "VENDOR",
ApplicationInfo.PRIVATE_FLAG_VIRTUAL_PRELOAD, "VIRTUAL_PRELOAD",
};
diff --git a/com/android/server/pm/ShortcutNonPersistentUser.java b/com/android/server/pm/ShortcutNonPersistentUser.java
new file mode 100644
index 00000000..7f6f684e
--- /dev/null
+++ b/com/android/server/pm/ShortcutNonPersistentUser.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.android.server.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.server.pm.ShortcutService.DumpFilter;
+
+import java.io.PrintWriter;
+
+/**
+ * This class holds per-user information for {@link ShortcutService} that doesn't have to be
+ * persisted and is kept in-memory.
+ *
+ * The access to it must be guarded with the shortcut manager lock.
+ */
+public class ShortcutNonPersistentUser {
+ private final ShortcutService mService;
+
+ private final int mUserId;
+
+ /**
+ * Keep track of additional packages that other parts of the system have said are
+ * allowed to access shortcuts. The key is the part of the system it came from,
+ * the value is the package name that has access. We don't persist these because
+ * at boot all relevant system services will push this data back to us they do their
+ * normal evaluation of the state of the world.
+ */
+ private final ArrayMap<String, String> mHostPackages = new ArrayMap<>();
+
+ /**
+ * Set of package name values from above.
+ */
+ private final ArraySet<String> mHostPackageSet = new ArraySet<>();
+
+ public ShortcutNonPersistentUser(ShortcutService service, int userId) {
+ mService = service;
+ mUserId = userId;
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ public void setShortcutHostPackage(@NonNull String type, @Nullable String packageName) {
+ if (packageName != null) {
+ mHostPackages.put(type, packageName);
+ } else {
+ mHostPackages.remove(type);
+ }
+
+ mHostPackageSet.clear();
+ for (int i = 0; i < mHostPackages.size(); i++) {
+ mHostPackageSet.add(mHostPackages.valueAt(i));
+ }
+ }
+
+ public boolean hasHostPackage(@NonNull String packageName) {
+ return mHostPackageSet.contains(packageName);
+ }
+
+ public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) {
+ if (filter.shouldDumpDetails()) {
+ if (mHostPackages.size() > 0) {
+ pw.print(prefix);
+ pw.print("Non-persistent: user ID:");
+ pw.println(mUserId);
+
+ pw.print(prefix);
+ pw.println(" Host packages:");
+ for (int i = 0; i < mHostPackages.size(); i++) {
+ pw.print(prefix);
+ pw.print(" ");
+ pw.print(mHostPackages.keyAt(i));
+ pw.print(": ");
+ pw.println(mHostPackages.valueAt(i));
+ }
+ pw.println();
+ }
+ }
+ }
+}
diff --git a/com/android/server/pm/ShortcutPackage.java b/com/android/server/pm/ShortcutPackage.java
index ba97c428..7bab3180 100644
--- a/com/android/server/pm/ShortcutPackage.java
+++ b/com/android/server/pm/ShortcutPackage.java
@@ -725,7 +725,7 @@ class ShortcutPackage extends ShortcutPackageItem {
// This means if a system app's version code doesn't change on an OTA,
// we don't notice it's updated. But that's fine since their version code *should*
// really change on OTAs.
- if ((getPackageInfo().getVersionCode() == pi.versionCode)
+ if ((getPackageInfo().getVersionCode() == pi.getLongVersionCode())
&& (getPackageInfo().getLastUpdateTime() == pi.lastUpdateTime)
&& areAllActivitiesStillEnabled()) {
return false;
@@ -759,11 +759,11 @@ class ShortcutPackage extends ShortcutPackageItem {
if (ShortcutService.DEBUG) {
Slog.d(TAG, String.format("Package %s %s, version %d -> %d", getPackageName(),
(isNewApp ? "added" : "updated"),
- getPackageInfo().getVersionCode(), pi.versionCode));
+ getPackageInfo().getVersionCode(), pi.getLongVersionCode()));
}
getPackageInfo().updateFromPackageInfo(pi);
- final int newVersionCode = getPackageInfo().getVersionCode();
+ final long newVersionCode = getPackageInfo().getVersionCode();
// See if there are any shortcuts that were prevented restoring because the app was of a
// lower version, and re-enable them.
@@ -1412,7 +1412,7 @@ class ShortcutPackage extends ShortcutPackageItem {
ShortcutService.writeAttr(out, ATTR_FLAGS, flags);
// Set the publisher version code at every backup.
- final int packageVersionCode = getPackageInfo().getVersionCode();
+ final long packageVersionCode = getPackageInfo().getVersionCode();
if (packageVersionCode == 0) {
s.wtf("Package version code should be available at this point.");
// However, 0 is a valid version code, so we just go ahead with it...
diff --git a/com/android/server/pm/ShortcutPackageInfo.java b/com/android/server/pm/ShortcutPackageInfo.java
index 3a9bbc89..3d372296 100644
--- a/com/android/server/pm/ShortcutPackageInfo.java
+++ b/com/android/server/pm/ShortcutPackageInfo.java
@@ -59,8 +59,8 @@ class ShortcutPackageInfo {
* been installed yet.
*/
private boolean mIsShadow;
- private int mVersionCode = ShortcutInfo.VERSION_CODE_UNKNOWN;
- private int mBackupSourceVersionCode = ShortcutInfo.VERSION_CODE_UNKNOWN;
+ private long mVersionCode = ShortcutInfo.VERSION_CODE_UNKNOWN;
+ private long mBackupSourceVersionCode = ShortcutInfo.VERSION_CODE_UNKNOWN;
private long mLastUpdateTime;
private ArrayList<byte[]> mSigHashes;
@@ -73,7 +73,7 @@ class ShortcutPackageInfo {
private boolean mBackupAllowed;
private boolean mBackupSourceBackupAllowed;
- private ShortcutPackageInfo(int versionCode, long lastUpdateTime,
+ private ShortcutPackageInfo(long versionCode, long lastUpdateTime,
ArrayList<byte[]> sigHashes, boolean isShadow) {
mVersionCode = versionCode;
mLastUpdateTime = lastUpdateTime;
@@ -96,11 +96,11 @@ class ShortcutPackageInfo {
mIsShadow = shadow;
}
- public int getVersionCode() {
+ public long getVersionCode() {
return mVersionCode;
}
- public int getBackupSourceVersionCode() {
+ public long getBackupSourceVersionCode() {
return mBackupSourceVersionCode;
}
@@ -123,7 +123,7 @@ class ShortcutPackageInfo {
*/
public void updateFromPackageInfo(@NonNull PackageInfo pi) {
if (pi != null) {
- mVersionCode = pi.versionCode;
+ mVersionCode = pi.getLongVersionCode();
mLastUpdateTime = pi.lastUpdateTime;
mBackupAllowed = ShortcutService.shouldBackupApp(pi);
mBackupAllowedInitialized = true;
@@ -145,10 +145,10 @@ class ShortcutPackageInfo {
Slog.w(TAG, "Can't restore: package didn't or doesn't allow backup");
return ShortcutInfo.DISABLED_REASON_BACKUP_NOT_SUPPORTED;
}
- if (!anyVersionOkay && (currentPackage.versionCode < mBackupSourceVersionCode)) {
+ if (!anyVersionOkay && (currentPackage.getLongVersionCode() < mBackupSourceVersionCode)) {
Slog.w(TAG, String.format(
"Can't restore: package current version %d < backed up version %d",
- currentPackage.versionCode, mBackupSourceVersionCode));
+ currentPackage.getLongVersionCode(), mBackupSourceVersionCode));
return ShortcutInfo.DISABLED_REASON_VERSION_LOWER;
}
return ShortcutInfo.DISABLED_REASON_NOT_DISABLED;
@@ -162,11 +162,12 @@ class ShortcutPackageInfo {
Slog.e(TAG, "Can't get signatures: package=" + packageName);
return null;
}
- final ShortcutPackageInfo ret = new ShortcutPackageInfo(pi.versionCode, pi.lastUpdateTime,
- BackupUtils.hashSignatureArray(pi.signatures), /* shadow=*/ false);
+ final ShortcutPackageInfo ret = new ShortcutPackageInfo(pi.getLongVersionCode(),
+ pi.lastUpdateTime, BackupUtils.hashSignatureArray(pi.signatures),
+ /* shadow=*/ false);
ret.mBackupSourceBackupAllowed = s.shouldBackupApp(pi);
- ret.mBackupSourceVersionCode = pi.versionCode;
+ ret.mBackupSourceVersionCode = pi.getLongVersionCode();
return ret;
}
@@ -213,7 +214,7 @@ class ShortcutPackageInfo {
throws IOException, XmlPullParserException {
// Don't use the version code from the backup file.
- final int versionCode = ShortcutService.parseIntAttribute(parser, ATTR_VERSION,
+ final long versionCode = ShortcutService.parseLongAttribute(parser, ATTR_VERSION,
ShortcutInfo.VERSION_CODE_UNKNOWN);
final long lastUpdateTime = ShortcutService.parseLongAttribute(
@@ -225,7 +226,7 @@ class ShortcutPackageInfo {
// We didn't used to save these attributes, and all backed up shortcuts were from
// apps that support backups, so the default values take this fact into consideration.
- final int backupSourceVersion = ShortcutService.parseIntAttribute(parser,
+ final long backupSourceVersion = ShortcutService.parseLongAttribute(parser,
ATTR_BACKUP_SOURCE_VERSION, ShortcutInfo.VERSION_CODE_UNKNOWN);
// Note the only time these "true" default value is used is when restoring from an old
diff --git a/com/android/server/pm/ShortcutPackageItem.java b/com/android/server/pm/ShortcutPackageItem.java
index 689099cf..0629d9e4 100644
--- a/com/android/server/pm/ShortcutPackageItem.java
+++ b/com/android/server/pm/ShortcutPackageItem.java
@@ -108,7 +108,7 @@ abstract class ShortcutPackageItem {
return; // Not installed, no need to restore yet.
}
int restoreBlockReason;
- int currentVersionCode = ShortcutInfo.VERSION_CODE_UNKNOWN;
+ long currentVersionCode = ShortcutInfo.VERSION_CODE_UNKNOWN;
if (!mPackageInfo.hasSignatures()) {
s.wtf("Attempted to restore package " + mPackageName + "/u" + mPackageUserId
@@ -116,7 +116,7 @@ abstract class ShortcutPackageItem {
restoreBlockReason = ShortcutInfo.DISABLED_REASON_SIGNATURE_MISMATCH;
} else {
final PackageInfo pi = s.getPackageInfoWithSignatures(mPackageName, mPackageUserId);
- currentVersionCode = pi.versionCode;
+ currentVersionCode = pi.getLongVersionCode();
restoreBlockReason = mPackageInfo.canRestoreTo(s, pi, canRestoreAnyVersion());
}
diff --git a/com/android/server/pm/ShortcutService.java b/com/android/server/pm/ShortcutService.java
index 0907dd7c..065eafd9 100644
--- a/com/android/server/pm/ShortcutService.java
+++ b/com/android/server/pm/ShortcutService.java
@@ -280,6 +280,13 @@ public class ShortcutService extends IShortcutService.Stub {
private final SparseArray<ShortcutUser> mUsers = new SparseArray<>();
/**
+ * User ID -> ShortcutNonPersistentUser
+ */
+ @GuardedBy("mLock")
+ private final SparseArray<ShortcutNonPersistentUser> mShortcutNonPersistentUsers =
+ new SparseArray<>();
+
+ /**
* Max number of dynamic + manifest shortcuts that each application can have at a time.
*/
private int mMaxShortcuts;
@@ -330,7 +337,10 @@ public class ShortcutService extends IShortcutService.Stub {
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
| PackageManager.MATCH_UNINSTALLED_PACKAGES;
- @GuardedBy("mLock")
+ /**
+ * Note we use a fine-grained lock for {@link #mUnlockedUsers} due to b/64303666.
+ */
+ @GuardedBy("mUnlockedUsers")
final SparseBooleanArray mUnlockedUsers = new SparseBooleanArray();
// Stats
@@ -600,7 +610,7 @@ public class ShortcutService extends IShortcutService.Stub {
if (DEBUG) {
Slog.d(TAG, "handleUnlockUser: user=" + userId);
}
- synchronized (mLock) {
+ synchronized (mUnlockedUsers) {
mUnlockedUsers.put(userId, true);
}
@@ -628,7 +638,9 @@ public class ShortcutService extends IShortcutService.Stub {
synchronized (mLock) {
unloadUserLocked(userId);
- mUnlockedUsers.put(userId, false);
+ synchronized (mUnlockedUsers) {
+ mUnlockedUsers.put(userId, false);
+ }
}
}
@@ -1149,9 +1161,12 @@ public class ShortcutService extends IShortcutService.Stub {
// Requires mLock held, but "Locked" prefix would look weired so we just say "L".
protected boolean isUserUnlockedL(@UserIdInt int userId) {
// First, check the local copy.
- if (mUnlockedUsers.get(userId)) {
- return true;
+ synchronized (mUnlockedUsers) {
+ if (mUnlockedUsers.get(userId)) {
+ return true;
+ }
}
+
// If the local copy says the user is locked, check with AM for the actual state, since
// the user might just have been unlocked.
// Note we just don't use isUserUnlockingOrUnlocked() here, because it'll return false
@@ -1199,6 +1214,18 @@ public class ShortcutService extends IShortcutService.Stub {
return userPackages;
}
+ /** Return the non-persistent per-user state. */
+ @GuardedBy("mLock")
+ @NonNull
+ ShortcutNonPersistentUser getNonPersistentUserLocked(@UserIdInt int userId) {
+ ShortcutNonPersistentUser ret = mShortcutNonPersistentUsers.get(userId);
+ if (ret == null) {
+ ret = new ShortcutNonPersistentUser(this, userId);
+ mShortcutNonPersistentUsers.put(userId, ret);
+ }
+ return ret;
+ }
+
void forEachLoadedUserLocked(@NonNull Consumer<ShortcutUser> c) {
for (int i = mUsers.size() - 1; i >= 0; i--) {
c.accept(mUsers.valueAt(i));
@@ -2251,7 +2278,7 @@ public class ShortcutService extends IShortcutService.Stub {
return true;
}
synchronized (mLock) {
- return getUserShortcutsLocked(userId).hasHostPackage(callingPackage);
+ return getNonPersistentUserLocked(userId).hasHostPackage(callingPackage);
}
}
@@ -2375,10 +2402,7 @@ public class ShortcutService extends IShortcutService.Stub {
public void setShortcutHostPackage(@NonNull String type, @Nullable String packageName,
int userId) {
synchronized (mLock) {
- throwIfUserLockedL(userId);
-
- final ShortcutUser user = getUserShortcutsLocked(userId);
- user.setShortcutHostPackage(type, packageName);
+ getNonPersistentUserLocked(userId).setShortcutHostPackage(type, packageName);
}
}
@@ -2739,6 +2763,26 @@ public class ShortcutService extends IShortcutService.Stub {
public boolean isRequestPinItemSupported(int callingUserId, int requestType) {
return ShortcutService.this.isRequestPinItemSupported(callingUserId, requestType);
}
+
+ @Override
+ public boolean isForegroundDefaultLauncher(@NonNull String callingPackage, int callingUid) {
+ Preconditions.checkNotNull(callingPackage);
+
+ final int userId = UserHandle.getUserId(callingUid);
+ final ComponentName defaultLauncher = getDefaultLauncher(userId);
+ if (defaultLauncher == null) {
+ return false;
+ }
+ if (!callingPackage.equals(defaultLauncher.getPackageName())) {
+ return false;
+ }
+ synchronized (mLock) {
+ if (!isUidForegroundLocked(callingUid)) {
+ return false;
+ }
+ }
+ return true;
+ }
}
final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@@ -3816,6 +3860,14 @@ public class ShortcutService extends IShortcutService.Stub {
pw.println();
}
}
+
+ for (int i = 0; i < mShortcutNonPersistentUsers.size(); i++) {
+ final ShortcutNonPersistentUser user = mShortcutNonPersistentUsers.valueAt(i);
+ if (filter.isUserMatch(user.getUserId())) {
+ user.dump(pw, " ", filter);
+ pw.println();
+ }
+ }
}
}
diff --git a/com/android/server/pm/ShortcutUser.java b/com/android/server/pm/ShortcutUser.java
index 1efd765b..b7247df3 100644
--- a/com/android/server/pm/ShortcutUser.java
+++ b/com/android/server/pm/ShortcutUser.java
@@ -124,20 +124,6 @@ class ShortcutUser {
/** In-memory-cached default launcher. */
private ComponentName mCachedLauncher;
- /**
- * Keep track of additional packages that other parts of the system have said are
- * allowed to access shortcuts. The key is the part of the system it came from,
- * the value is the package name that has access. We don't persist these because
- * at boot all relevant system services will push this data back to us they do their
- * normal evaluation of the state of the world.
- */
- private final ArrayMap<String, String> mHostPackages = new ArrayMap<>();
-
- /**
- * Set of package name values from above.
- */
- private final ArraySet<String> mHostPackageSet = new ArraySet<>();
-
private String mKnownLocales;
private long mLastAppScanTime;
@@ -482,23 +468,6 @@ class ShortcutUser {
return mCachedLauncher;
}
- public void setShortcutHostPackage(@NonNull String type, @Nullable String packageName) {
- if (packageName != null) {
- mHostPackages.put(type, packageName);
- } else {
- mHostPackages.remove(type);
- }
-
- mHostPackageSet.clear();
- for (int i = 0; i < mHostPackages.size(); i++) {
- mHostPackageSet.add(mHostPackages.valueAt(i));
- }
- }
-
- public boolean hasHostPackage(@NonNull String packageName) {
- return mHostPackageSet.contains(packageName);
- }
-
public void resetThrottling() {
for (int i = mPackages.size() - 1; i >= 0; i--) {
mPackages.valueAt(i).resetThrottling();
@@ -587,18 +556,6 @@ class ShortcutUser {
pw.print("Last known launcher: ");
pw.print(mLastKnownLauncher);
pw.println();
-
- if (mHostPackages.size() > 0) {
- pw.print(prefix);
- pw.println("Host packages:");
- for (int i = 0; i < mHostPackages.size(); i++) {
- pw.print(prefix);
- pw.print(" ");
- pw.print(mHostPackages.keyAt(i));
- pw.print(": ");
- pw.println(mHostPackages.valueAt(i));
- }
- }
}
for (int i = 0; i < mLaunchers.size(); i++) {
diff --git a/com/android/server/pm/UserManagerService.java b/com/android/server/pm/UserManagerService.java
index dbf413f0..768eb8f3 100644
--- a/com/android/server/pm/UserManagerService.java
+++ b/com/android/server/pm/UserManagerService.java
@@ -27,7 +27,7 @@ import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityManagerNative;
-import android.app.AppGlobals;
+import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.IStopUserCallback;
import android.app.KeyguardManager;
@@ -39,6 +39,7 @@ import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ShortcutServiceInternal;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -50,6 +51,7 @@ import android.os.Environment;
import android.os.FileUtils;
import android.os.Handler;
import android.os.IBinder;
+import android.os.IProgressListener;
import android.os.IUserManager;
import android.os.Message;
import android.os.ParcelFileDescriptor;
@@ -71,10 +73,6 @@ import android.os.UserManagerInternal.UserRestrictionsListener;
import android.os.storage.StorageManager;
import android.security.GateKeeper;
import android.service.gatekeeper.IGateKeeperService;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.system.OsConstants;
-import android.text.TextUtils;
import android.util.AtomicFile;
import android.util.IntArray;
import android.util.Log;
@@ -113,9 +111,9 @@ import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -378,22 +376,45 @@ public class UserManagerService extends IUserManager.Stub {
private final BroadcastReceiver mDisableQuietModeCallback = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK.equals(intent.getAction())) {
- final IntentSender target = intent.getParcelableExtra(Intent.EXTRA_INTENT);
- final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_ID, 0);
- setQuietModeEnabled(userHandle, false);
- if (target != null) {
- try {
- mContext.startIntentSender(target, null, 0, 0, 0);
- } catch (IntentSender.SendIntentException e) {
- /* ignore */
- }
- }
+ if (!ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK.equals(intent.getAction())) {
+ return;
}
+ final IntentSender target = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+ final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL);
+ setQuietModeEnabled(userHandle, false, target);
}
};
/**
+ * Start an {@link IntentSender} when user is unlocked after disabling quiet mode.
+ *
+ * @see {@link #trySetQuietModeEnabled(String, boolean, int, IntentSender)}
+ */
+ private class DisableQuietModeUserUnlockedCallback extends IProgressListener.Stub {
+ private final IntentSender mTarget;
+
+ public DisableQuietModeUserUnlockedCallback(IntentSender target) {
+ Preconditions.checkNotNull(target);
+ mTarget = target;
+ }
+
+ @Override
+ public void onStarted(int id, Bundle extras) {}
+
+ @Override
+ public void onProgress(int id, int progress, Bundle extras) {}
+
+ @Override
+ public void onFinished(int id, Bundle extras) {
+ try {
+ mContext.startIntentSender(mTarget, null, 0, 0, 0);
+ } catch (IntentSender.SendIntentException e) {
+ Slog.e(LOG_TAG, "Failed to start the target in the callback", e);
+ }
+ }
+ }
+
+ /**
* Whether all users should be created ephemeral.
*/
@GuardedBy("mUsersLock")
@@ -765,44 +786,114 @@ public class UserManagerService extends IUserManager.Stub {
}
@Override
- public void setQuietModeEnabled(int userHandle, boolean enableQuietMode) {
- checkManageUsersPermission("silence profile");
- boolean changed = false;
- UserInfo profile, parent;
- synchronized (mPackagesLock) {
- synchronized (mUsersLock) {
- profile = getUserInfoLU(userHandle);
- parent = getProfileParentLU(userHandle);
+ public boolean trySetQuietModeEnabled(@NonNull String callingPackage, boolean enableQuietMode,
+ int userHandle, @Nullable IntentSender target) {
+ Preconditions.checkNotNull(callingPackage);
+
+ if (enableQuietMode && target != null) {
+ throw new IllegalArgumentException(
+ "target should only be specified when we are disabling quiet mode.");
+ }
+
+ if (!isAllowedToSetWorkMode(callingPackage, Binder.getCallingUid())) {
+ throw new SecurityException("Not allowed to call trySetQuietModeEnabled, "
+ + "caller is foreground default launcher "
+ + "nor with MANAGE_USERS/MODIFY_QUIET_MODE permission");
+ }
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ if (enableQuietMode) {
+ setQuietModeEnabled(userHandle, true /* enableQuietMode */, target);
+ return true;
+ } else {
+ boolean needToShowConfirmCredential =
+ mLockPatternUtils.isSecure(userHandle)
+ && !StorageManager.isUserKeyUnlocked(userHandle);
+ if (needToShowConfirmCredential) {
+ showConfirmCredentialToDisableQuietMode(userHandle, target);
+ return false;
+ } else {
+ setQuietModeEnabled(userHandle, false /* enableQuietMode */, target);
+ return true;
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ /**
+ * An app can modify quiet mode if the caller meets one of the condition:
+ * <ul>
+ * <li>Has system UID or root UID</li>
+ * <li>Has {@link Manifest.permission#MODIFY_QUIET_MODE}</li>
+ * <li>Has {@link Manifest.permission#MANAGE_USERS}</li>
+ * </ul>
+ */
+ private boolean isAllowedToSetWorkMode(String callingPackage, int callingUid) {
+ if (hasManageUsersPermission()) {
+ return true;
+ }
+
+ final boolean hasModifyQuietModePermission = ActivityManager.checkComponentPermission(
+ Manifest.permission.MODIFY_QUIET_MODE,
+ callingUid, -1, true) == PackageManager.PERMISSION_GRANTED;
+ if (hasModifyQuietModePermission) {
+ return true;
+ }
+
+ final ShortcutServiceInternal shortcutInternal =
+ LocalServices.getService(ShortcutServiceInternal.class);
+ if (shortcutInternal != null) {
+ boolean isForegroundLauncher =
+ shortcutInternal.isForegroundDefaultLauncher(callingPackage, callingUid);
+ if (isForegroundLauncher) {
+ return true;
}
+ }
+ return false;
+ }
+
+ private void setQuietModeEnabled(
+ int userHandle, boolean enableQuietMode, IntentSender target) {
+ final UserInfo profile, parent;
+ final UserData profileUserData;
+ synchronized (mUsersLock) {
+ profile = getUserInfoLU(userHandle);
+ parent = getProfileParentLU(userHandle);
+
if (profile == null || !profile.isManagedProfile()) {
throw new IllegalArgumentException("User " + userHandle + " is not a profile");
}
- if (profile.isQuietModeEnabled() != enableQuietMode) {
- profile.flags ^= UserInfo.FLAG_QUIET_MODE;
- writeUserLP(getUserDataLU(profile.id));
- changed = true;
+ if (profile.isQuietModeEnabled() == enableQuietMode) {
+ Slog.i(LOG_TAG, "Quiet mode is already " + enableQuietMode);
+ return;
}
+ profile.flags ^= UserInfo.FLAG_QUIET_MODE;
+ profileUserData = getUserDataLU(profile.id);
}
- if (changed) {
- long identity = Binder.clearCallingIdentity();
- try {
- if (enableQuietMode) {
- ActivityManager.getService().stopUser(userHandle, /* force */true, null);
- LocalServices.getService(ActivityManagerInternal.class)
- .killForegroundAppsForUser(userHandle);
- } else {
- ActivityManager.getService().startUserInBackground(userHandle);
- }
- } catch (RemoteException e) {
- Slog.e(LOG_TAG, "fail to start/stop user for quiet mode", e);
- } finally {
- Binder.restoreCallingIdentity(identity);
+ synchronized (mPackagesLock) {
+ writeUserLP(profileUserData);
+ }
+ try {
+ if (enableQuietMode) {
+ ActivityManager.getService().stopUser(userHandle, /* force */true, null);
+ LocalServices.getService(ActivityManagerInternal.class)
+ .killForegroundAppsForUser(userHandle);
+ } else {
+ IProgressListener callback = target != null
+ ? new DisableQuietModeUserUnlockedCallback(target)
+ : null;
+ ActivityManager.getService().startUserInBackgroundWithListener(
+ userHandle, callback);
}
-
- broadcastProfileAvailabilityChanges(profile.getUserHandle(), parent.getUserHandle(),
- enableQuietMode);
+ } catch (RemoteException e) {
+ // Should not happen, same process.
+ e.rethrowAsRuntimeException();
}
+ broadcastProfileAvailabilityChanges(profile.getUserHandle(), parent.getUserHandle(),
+ enableQuietMode);
}
@Override
@@ -819,53 +910,42 @@ public class UserManagerService extends IUserManager.Stub {
}
}
- @Override
- public boolean trySetQuietModeDisabled(int userHandle, IntentSender target) {
- checkManageUsersPermission("silence profile");
- if (StorageManager.isUserKeyUnlocked(userHandle)
- || !mLockPatternUtils.isSecure(userHandle)) {
- // if the user is already unlocked, no need to show a profile challenge
- setQuietModeEnabled(userHandle, false);
- return true;
- }
-
- long identity = Binder.clearCallingIdentity();
- try {
- // otherwise, we show a profile challenge to trigger decryption of the user
- final KeyguardManager km = (KeyguardManager) mContext.getSystemService(
- Context.KEYGUARD_SERVICE);
- // We should use userHandle not credentialOwnerUserId here, as even if it is unified
- // lock, confirm screenlock page will know and show personal challenge, and unlock
- // work profile when personal challenge is correct
- final Intent unlockIntent = km.createConfirmDeviceCredentialIntent(null, null,
- userHandle);
- if (unlockIntent == null) {
- return false;
- }
- final Intent callBackIntent = new Intent(
- ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK);
- if (target != null) {
- callBackIntent.putExtra(Intent.EXTRA_INTENT, target);
- }
- callBackIntent.putExtra(Intent.EXTRA_USER_ID, userHandle);
- callBackIntent.setPackage(mContext.getPackageName());
- callBackIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- final PendingIntent pendingIntent = PendingIntent.getBroadcast(
- mContext,
- 0,
- callBackIntent,
- PendingIntent.FLAG_CANCEL_CURRENT |
- PendingIntent.FLAG_ONE_SHOT |
- PendingIntent.FLAG_IMMUTABLE);
- // After unlocking the challenge, it will disable quiet mode and run the original
- // intentSender
- unlockIntent.putExtra(Intent.EXTRA_INTENT, pendingIntent.getIntentSender());
- unlockIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- mContext.startActivity(unlockIntent);
- } finally {
- Binder.restoreCallingIdentity(identity);
+ /**
+ * Show confirm credential screen to unlock user in order to turn off quiet mode.
+ */
+ private void showConfirmCredentialToDisableQuietMode(
+ @UserIdInt int userHandle, @Nullable IntentSender target) {
+ // otherwise, we show a profile challenge to trigger decryption of the user
+ final KeyguardManager km = (KeyguardManager) mContext.getSystemService(
+ Context.KEYGUARD_SERVICE);
+ // We should use userHandle not credentialOwnerUserId here, as even if it is unified
+ // lock, confirm screenlock page will know and show personal challenge, and unlock
+ // work profile when personal challenge is correct
+ final Intent unlockIntent = km.createConfirmDeviceCredentialIntent(null, null,
+ userHandle);
+ if (unlockIntent == null) {
+ return;
}
- return false;
+ final Intent callBackIntent = new Intent(
+ ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK);
+ if (target != null) {
+ callBackIntent.putExtra(Intent.EXTRA_INTENT, target);
+ }
+ callBackIntent.putExtra(Intent.EXTRA_USER_ID, userHandle);
+ callBackIntent.setPackage(mContext.getPackageName());
+ callBackIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ mContext,
+ 0,
+ callBackIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT |
+ PendingIntent.FLAG_ONE_SHOT |
+ PendingIntent.FLAG_IMMUTABLE);
+ // After unlocking the challenge, it will disable quiet mode and run the original
+ // intentSender
+ unlockIntent.putExtra(Intent.EXTRA_INTENT, pendingIntent.getIntentSender());
+ unlockIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ mContext.startActivity(unlockIntent);
}
@Override
@@ -3678,8 +3758,10 @@ public class UserManagerService extends IUserManager.Stub {
}
@Override
- public UserInfo createUserEvenWhenDisallowed(String name, int flags) {
- UserInfo user = createUserInternalUnchecked(name, flags, UserHandle.USER_NULL, null);
+ public UserInfo createUserEvenWhenDisallowed(String name, int flags,
+ String[] disallowedPackages) {
+ UserInfo user = createUserInternalUnchecked(name, flags, UserHandle.USER_NULL,
+ disallowedPackages);
// Keep this in sync with UserManager.createUser
if (user != null && !user.isAdmin() && !user.isDemo()) {
setUserRestriction(UserManager.DISALLOW_SMS, true, user.id);
diff --git a/com/android/server/pm/UserRestrictionsUtils.java b/com/android/server/pm/UserRestrictionsUtils.java
index 4b134043..92408432 100644
--- a/com/android/server/pm/UserRestrictionsUtils.java
+++ b/com/android/server/pm/UserRestrictionsUtils.java
@@ -113,7 +113,10 @@ public class UserRestrictionsUtils {
UserManager.DISALLOW_OEM_UNLOCK,
UserManager.DISALLOW_UNMUTE_DEVICE,
UserManager.DISALLOW_AUTOFILL,
- UserManager.DISALLOW_USER_SWITCH
+ UserManager.DISALLOW_USER_SWITCH,
+ UserManager.DISALLOW_UNIFIED_PASSWORD,
+ UserManager.DISALLOW_CONFIG_LOCATION_MODE,
+ UserManager.DISALLOW_AIRPLANE_MODE
});
/**
@@ -141,7 +144,8 @@ public class UserRestrictionsUtils {
UserManager.DISALLOW_FUN,
UserManager.DISALLOW_SAFE_BOOT,
UserManager.DISALLOW_CREATE_WINDOWS,
- UserManager.DISALLOW_DATA_ROAMING
+ UserManager.DISALLOW_DATA_ROAMING,
+ UserManager.DISALLOW_AIRPLANE_MODE
);
/**
@@ -192,11 +196,12 @@ public class UserRestrictionsUtils {
UserManager.DISALLOW_BLUETOOTH_SHARING
);
- /*
+ /**
* Special user restrictions that are always applied to all users no matter who sets them.
*/
private static final Set<String> PROFILE_GLOBAL_RESTRICTIONS = Sets.newArraySet(
- UserManager.ENSURE_VERIFY_APPS
+ UserManager.ENSURE_VERIFY_APPS,
+ UserManager.DISALLOW_AIRPLANE_MODE
);
/**
diff --git a/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java b/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java
index 854b7043..d6281c51 100644
--- a/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java
+++ b/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java
@@ -19,6 +19,7 @@ import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import android.annotation.UserIdInt;
+import android.app.ActivityOptions;
import android.app.AppOpsManager;
import android.content.ComponentName;
import android.content.Context;
@@ -74,8 +75,6 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub {
public void startActivityAsUser(
String callingPackage,
ComponentName component,
- Rect sourceBounds,
- Bundle startActivityOptions,
UserHandle user) throws RemoteException {
Preconditions.checkNotNull(callingPackage);
Preconditions.checkNotNull(component);
@@ -103,7 +102,6 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub {
// CATEGORY_LAUNCHER as calling startActivityAsUser ignore them if component is present.
final Intent launchIntent = new Intent(Intent.ACTION_MAIN);
launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- launchIntent.setSourceBounds(sourceBounds);
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
// Only package name is set here, as opposed to component name, because intent action and
@@ -114,7 +112,8 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub {
final long ident = mInjector.clearCallingIdentity();
try {
launchIntent.setComponent(component);
- mContext.startActivityAsUser(launchIntent, startActivityOptions, user);
+ mContext.startActivityAsUser(launchIntent,
+ ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(), user);
} finally {
mInjector.restoreCallingIdentity(ident);
}
diff --git a/com/android/server/pm/dex/ArtManagerService.java b/com/android/server/pm/dex/ArtManagerService.java
new file mode 100644
index 00000000..2dbb34d8
--- /dev/null
+++ b/com/android/server/pm/dex/ArtManagerService.java
@@ -0,0 +1,233 @@
+/*
+ * 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 com.android.server.pm.dex;
+
+import android.Manifest;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.dex.ArtManager;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.content.pm.IPackageManager;
+import android.content.pm.dex.ISnapshotRuntimeProfileCallback;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.Preconditions;
+import com.android.server.pm.Installer;
+import com.android.server.pm.Installer.InstallerException;
+import java.io.File;
+import java.io.FileNotFoundException;
+
+/**
+ * A system service that provides access to runtime and compiler artifacts.
+ *
+ * This service is not accessed by users directly, instead one uses an instance of
+ * {@link ArtManager}, which can be accessed via {@link PackageManager} as follows:
+ * <p/>
+ * {@code context().getPackageManager().getArtManager();}
+ * <p class="note">
+ * Note: Accessing runtime artifacts may require extra permissions. For example querying the
+ * runtime profiles of apps requires {@link android.Manifest.permission#READ_RUNTIME_PROFILES}
+ * which is a system-level permission that will not be granted to normal apps.
+ */
+public class ArtManagerService extends android.content.pm.dex.IArtManager.Stub {
+ private static final String TAG = "ArtManagerService";
+
+ private static boolean DEBUG = false;
+ private static boolean DEBUG_IGNORE_PERMISSIONS = false;
+
+ private final IPackageManager mPackageManager;
+ private final Object mInstallLock;
+ @GuardedBy("mInstallLock")
+ private final Installer mInstaller;
+
+ private final Handler mHandler;
+
+ public ArtManagerService(IPackageManager pm, Installer installer, Object installLock) {
+ mPackageManager = pm;
+ mInstaller = installer;
+ mInstallLock = installLock;
+ mHandler = new Handler(BackgroundThread.getHandler().getLooper());
+ }
+
+ @Override
+ public void snapshotRuntimeProfile(String packageName, String codePath,
+ ISnapshotRuntimeProfileCallback callback) {
+ // Sanity checks on the arguments.
+ Preconditions.checkStringNotEmpty(packageName);
+ Preconditions.checkStringNotEmpty(codePath);
+ Preconditions.checkNotNull(callback);
+
+ // Verify that the caller has the right permissions.
+ checkReadRuntimeProfilePermission();
+
+ if (DEBUG) {
+ Slog.d(TAG, "Requested snapshot for " + packageName + ":" + codePath);
+ }
+
+ PackageInfo info = null;
+ try {
+ // Note that we use the default user 0 to retrieve the package info.
+ // This doesn't really matter because for user 0 we always get a package back (even if
+ // it's not installed for the user 0). It is ok because we only care about the code
+ // paths and not if the package is enabled or not for the user.
+
+ // TODO(calin): consider adding an API to PMS which can retrieve the
+ // PackageParser.Package.
+ info = mPackageManager.getPackageInfo(packageName, /*flags*/ 0, /*userId*/ 0);
+ } catch (RemoteException ignored) {
+ // Should not happen.
+ }
+ if (info == null) {
+ postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_PACKAGE_NOT_FOUND);
+ return;
+ }
+
+ boolean pathFound = info.applicationInfo.getBaseCodePath().equals(codePath);
+ String[] splitCodePaths = info.applicationInfo.getSplitCodePaths();
+ if (!pathFound && (splitCodePaths != null)) {
+ for (String path : splitCodePaths) {
+ if (path.equals(codePath)) {
+ pathFound = true;
+ break;
+ }
+ }
+ }
+ if (!pathFound) {
+ postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_CODE_PATH_NOT_FOUND);
+ return;
+ }
+
+ // All good, create the profile snapshot.
+ createProfileSnapshot(packageName, codePath, callback, info);
+ // Destroy the snapshot, we no longer need it.
+ destroyProfileSnapshot(packageName, codePath);
+ }
+
+ private void createProfileSnapshot(String packageName, String codePath,
+ ISnapshotRuntimeProfileCallback callback, PackageInfo info) {
+ // Ask the installer to snapshot the profile.
+ synchronized (mInstallLock) {
+ try {
+ if (!mInstaller.createProfileSnapshot(UserHandle.getAppId(info.applicationInfo.uid),
+ packageName, codePath)) {
+ postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR);
+ return;
+ }
+ } catch (InstallerException e) {
+ postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR);
+ return;
+ }
+ }
+
+ // Open the snapshot and invoke the callback.
+ File snapshotProfile = Environment.getProfileSnapshotPath(packageName, codePath);
+ ParcelFileDescriptor fd;
+ try {
+ fd = ParcelFileDescriptor.open(snapshotProfile, ParcelFileDescriptor.MODE_READ_ONLY);
+ postSuccess(packageName, fd, callback);
+ } catch (FileNotFoundException e) {
+ Slog.w(TAG, "Could not open snapshot profile for " + packageName + ":" + codePath, e);
+ postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR);
+ }
+ }
+
+ private void destroyProfileSnapshot(String packageName, String codePath) {
+ if (DEBUG) {
+ Slog.d(TAG, "Destroying profile snapshot for" + packageName + ":" + codePath);
+ }
+
+ synchronized (mInstallLock) {
+ try {
+ mInstaller.destroyProfileSnapshot(packageName, codePath);
+ } catch (InstallerException e) {
+ Slog.e(TAG, "Failed to destroy profile snapshot for " +
+ packageName + ":" + codePath, e);
+ }
+ }
+ }
+
+ @Override
+ public boolean isRuntimeProfilingEnabled() {
+ // Verify that the caller has the right permissions.
+ checkReadRuntimeProfilePermission();
+
+ return SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false);
+ }
+
+ /**
+ * Post {@link ISnapshotRuntimeProfileCallback#onError(int)} with the given error message
+ * on the internal {@code mHandler}.
+ */
+ private void postError(ISnapshotRuntimeProfileCallback callback, String packageName,
+ int errCode) {
+ if (DEBUG) {
+ Slog.d(TAG, "Failed to snapshot profile for " + packageName + " with error: " +
+ errCode);
+ }
+ mHandler.post(() -> {
+ try {
+ callback.onError(errCode);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to callback after profile snapshot for " + packageName, e);
+ }
+ });
+ }
+
+ private void postSuccess(String packageName, ParcelFileDescriptor fd,
+ ISnapshotRuntimeProfileCallback callback) {
+ if (DEBUG) {
+ Slog.d(TAG, "Successfully snapshot profile for " + packageName);
+ }
+ mHandler.post(() -> {
+ try {
+ callback.onSuccess(fd);
+ } catch (RemoteException e) {
+ Slog.w(TAG,
+ "Failed to call onSuccess after profile snapshot for " + packageName, e);
+ }
+ });
+ }
+
+ /**
+ * Verify that the binder calling uid has {@code android.permission.READ_RUNTIME_PROFILE}.
+ * If not, it throws a {@link SecurityException}.
+ */
+ private void checkReadRuntimeProfilePermission() {
+ if (DEBUG_IGNORE_PERMISSIONS) {
+ return;
+ }
+ try {
+ int result = mPackageManager.checkUidPermission(
+ Manifest.permission.READ_RUNTIME_PROFILES, Binder.getCallingUid());
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("You need "
+ + Manifest.permission.READ_RUNTIME_PROFILES
+ + " permission to snapshot profiles.");
+ }
+ } catch (RemoteException e) {
+ // Should not happen.
+ }
+ }
+}
diff --git a/com/android/server/pm/permission/BasePermission.java b/com/android/server/pm/permission/BasePermission.java
index 8c86db64..75a61064 100644
--- a/com/android/server/pm/permission/BasePermission.java
+++ b/com/android/server/pm/permission/BasePermission.java
@@ -225,6 +225,9 @@ public final class BasePermission {
public boolean isVerifier() {
return (protectionLevel & PermissionInfo.PROTECTION_FLAG_VERIFIER) != 0;
}
+ public boolean isVendorPrivileged() {
+ return (protectionLevel & PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED) != 0;
+ }
public void transfer(@NonNull String origPackageName, @NonNull String newPackageName) {
if (!origPackageName.equals(sourcePackageName)) {
diff --git a/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index c40d1fad..d38dc9ae 100644
--- a/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -30,6 +30,7 @@ import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
+import android.content.pm.PackageList;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.PackageParser;
@@ -252,11 +253,11 @@ public final class DefaultPermissionGrantPolicy {
}
}
- public void grantDefaultPermissions(Collection<PackageParser.Package> packages, int userId) {
+ public void grantDefaultPermissions(int userId) {
if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_EMBEDDED, 0)) {
- grantAllRuntimePermissions(packages, userId);
+ grantAllRuntimePermissions(userId);
} else {
- grantPermissionsToSysComponentsAndPrivApps(packages, userId);
+ grantPermissionsToSysComponentsAndPrivApps(userId);
grantDefaultSystemHandlerPermissions(userId);
grantDefaultPermissionExceptions(userId);
}
@@ -278,10 +279,14 @@ public final class DefaultPermissionGrantPolicy {
}
}
- private void grantAllRuntimePermissions(
- Collection<PackageParser.Package> packages, int userId) {
+ private void grantAllRuntimePermissions(int userId) {
Log.i(TAG, "Granting all runtime permissions for user " + userId);
- for (PackageParser.Package pkg : packages) {
+ final PackageList packageList = mServiceInternal.getPackageList();
+ for (String packageName : packageList.getPackageNames()) {
+ final PackageParser.Package pkg = mServiceInternal.getPackage(packageName);
+ if (pkg == null) {
+ continue;
+ }
grantRuntimePermissionsForPackage(userId, pkg);
}
}
@@ -290,10 +295,14 @@ public final class DefaultPermissionGrantPolicy {
mHandler.sendEmptyMessage(MSG_READ_DEFAULT_PERMISSION_EXCEPTIONS);
}
- private void grantPermissionsToSysComponentsAndPrivApps(
- Collection<PackageParser.Package> packages, int userId) {
+ private void grantPermissionsToSysComponentsAndPrivApps(int userId) {
Log.i(TAG, "Granting permissions to platform components for user " + userId);
- for (PackageParser.Package pkg : packages) {
+ final PackageList packageList = mServiceInternal.getPackageList();
+ for (String packageName : packageList.getPackageNames()) {
+ final PackageParser.Package pkg = mServiceInternal.getPackage(packageName);
+ if (pkg == null) {
+ continue;
+ }
if (!isSysComponentOrPersistentPlatformSignedPrivApp(pkg)
|| !doesPackageSupportRuntimePermissions(pkg)
|| pkg.requestedPermissions.isEmpty()) {
@@ -640,9 +649,9 @@ public final class DefaultPermissionGrantPolicy {
if (globalSearchPickerPackage != null
&& doesPackageSupportRuntimePermissions(globalSearchPickerPackage)) {
grantRuntimePermissions(globalSearchPickerPackage,
- MICROPHONE_PERMISSIONS, true, userId);
+ MICROPHONE_PERMISSIONS, false, userId);
grantRuntimePermissions(globalSearchPickerPackage,
- LOCATION_PERMISSIONS, true, userId);
+ LOCATION_PERMISSIONS, false, userId);
}
}
diff --git a/com/android/server/pm/permission/PermissionManagerService.java b/com/android/server/pm/permission/PermissionManagerService.java
index 7d8e2069..90ac4ab7 100644
--- a/com/android/server/pm/permission/PermissionManagerService.java
+++ b/com/android/server/pm/permission/PermissionManagerService.java
@@ -959,8 +959,9 @@ Slog.e(TAG, "TODD: Packages: " + Arrays.toString(packages));
* <p>This handles parent/child apps.
*/
private boolean hasPrivappWhitelistEntry(String perm, PackageParser.Package pkg) {
- ArraySet<String> wlPermissions = SystemConfig.getInstance()
- .getPrivAppPermissions(pkg.packageName);
+ ArraySet<String> wlPermissions = pkg.isVendor() ?
+ SystemConfig.getInstance().getVendorPrivAppPermissions(pkg.packageName)
+ : SystemConfig.getInstance().getPrivAppPermissions(pkg.packageName);
// Let's check if this package is whitelisted...
boolean whitelisted = wlPermissions != null && wlPermissions.contains(perm);
// If it's not, we'll also tail-recurse to the parent.
@@ -971,7 +972,8 @@ Slog.e(TAG, "TODD: Packages: " + Arrays.toString(packages));
private boolean grantSignaturePermission(String perm, PackageParser.Package pkg,
BasePermission bp, PermissionsState origPermissions) {
boolean oemPermission = bp.isOEM();
- boolean privilegedPermission = bp.isPrivileged();
+ boolean vendorPrivilegedPermission = bp.isVendorPrivileged();
+ boolean privilegedPermission = bp.isPrivileged() || bp.isVendorPrivileged();
boolean privappPermissionsDisable =
RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_DISABLE;
boolean platformPermission = PLATFORM_PACKAGE_NAME.equals(bp.getSourcePackageName());
@@ -982,8 +984,11 @@ Slog.e(TAG, "TODD: Packages: " + Arrays.toString(packages));
// Only report violations for apps on system image
if (!mSystemReady && !pkg.isUpdatedSystemApp()) {
// it's only a reportable violation if the permission isn't explicitly denied
- final ArraySet<String> deniedPermissions = SystemConfig.getInstance()
- .getPrivAppDenyPermissions(pkg.packageName);
+ final ArraySet<String> deniedPermissions = pkg.isVendor() ?
+ SystemConfig.getInstance()
+ .getVendorPrivAppDenyPermissions(pkg.packageName)
+ : SystemConfig.getInstance()
+ .getPrivAppDenyPermissions(pkg.packageName);
final boolean permissionViolation =
deniedPermissions == null || !deniedPermissions.contains(perm);
if (permissionViolation) {
@@ -1086,6 +1091,15 @@ Slog.e(TAG, "TODD: Packages: " + Arrays.toString(packages));
|| (oemPermission && pkg.isOem()
&& canGrantOemPermission(ps, perm));
}
+ // In any case, don't grant a privileged permission to privileged vendor apps, if
+ // the permission's protectionLevel does not have the extra 'vendorPrivileged'
+ // flag.
+ if (allowed && privilegedPermission &&
+ !vendorPrivilegedPermission && pkg.isVendor()) {
+ Slog.w(TAG, "Permission " + perm + " cannot be granted to privileged vendor apk "
+ + pkg.packageName + " because it isn't a 'vendorPrivileged' permission.");
+ allowed = false;
+ }
}
}
if (!allowed) {
diff --git a/com/android/server/policy/PhoneWindowManager.java b/com/android/server/policy/PhoneWindowManager.java
index 7415ec38..076c0e4d 100644
--- a/com/android/server/policy/PhoneWindowManager.java
+++ b/com/android/server/policy/PhoneWindowManager.java
@@ -48,6 +48,7 @@ import static android.view.WindowManager.INPUT_CONSUMER_NAVIGATION;
import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
import static android.view.WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA;
import static android.view.WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
import static android.view.WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN;
@@ -211,6 +212,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
+import android.view.DisplayCutout;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.IApplicationToken;
@@ -239,6 +241,7 @@ import android.view.autofill.AutofillManagerInternal;
import android.view.inputmethod.InputMethodManagerInternal;
import com.android.internal.R;
+import com.android.internal.accessibility.AccessibilityShortcutController;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
@@ -246,6 +249,7 @@ import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.internal.policy.IShortcutService;
import com.android.internal.policy.PhoneWindow;
import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ScreenShapeHelper;
import com.android.internal.widget.PointerLocationView;
import com.android.server.GestureLauncherService;
@@ -317,11 +321,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
static final int LONG_PRESS_BACK_NOTHING = 0;
static final int LONG_PRESS_BACK_GO_TO_VOICE_ASSIST = 1;
- // Number of presses needed before we induce panic press behavior on the back button
- static final int PANIC_PRESS_BACK_COUNT = 4;
- static final int PANIC_PRESS_BACK_NOTHING = 0;
- static final int PANIC_PRESS_BACK_HOME = 1;
-
// These need to match the documentation/constant in
// core/res/res/values/config.xml
static final int LONG_PRESS_HOME_NOTHING = 0;
@@ -516,7 +515,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
volatile boolean mBackKeyHandled;
volatile boolean mBeganFromNonInteractive;
volatile int mPowerKeyPressCounter;
- volatile int mBackKeyPressCounter;
volatile boolean mEndCallKeyHandled;
volatile boolean mCameraGestureTriggeredDuringGoingToSleep;
volatile boolean mGoingToSleep;
@@ -578,7 +576,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
int mDoublePressOnPowerBehavior;
int mTriplePressOnPowerBehavior;
int mLongPressOnBackBehavior;
- int mPanicPressOnBackBehavior;
int mShortPressOnSleepBehavior;
int mShortPressOnWindowBehavior;
volatile boolean mAwake;
@@ -604,6 +601,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
PointerLocationView mPointerLocationView;
+ boolean mEmulateDisplayCutout = false;
+
// During layout, the layer at which the doc window is placed.
int mDockLayer;
// During layout, this is the layer of the status bar.
@@ -794,16 +793,15 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private static final int MSG_SHOW_PICTURE_IN_PICTURE_MENU = 17;
private static final int MSG_BACK_LONG_PRESS = 18;
private static final int MSG_DISPOSE_INPUT_CONSUMER = 19;
- private static final int MSG_BACK_DELAYED_PRESS = 20;
- private static final int MSG_ACCESSIBILITY_SHORTCUT = 21;
- private static final int MSG_BUGREPORT_TV = 22;
- private static final int MSG_ACCESSIBILITY_TV = 23;
- private static final int MSG_DISPATCH_BACK_KEY_TO_AUTOFILL = 24;
- private static final int MSG_SYSTEM_KEY_PRESS = 25;
- private static final int MSG_HANDLE_ALL_APPS = 26;
- private static final int MSG_LAUNCH_ASSIST = 27;
- private static final int MSG_LAUNCH_ASSIST_LONG_PRESS = 28;
- private static final int MSG_POWER_VERY_LONG_PRESS = 29;
+ private static final int MSG_ACCESSIBILITY_SHORTCUT = 20;
+ private static final int MSG_BUGREPORT_TV = 21;
+ private static final int MSG_ACCESSIBILITY_TV = 22;
+ private static final int MSG_DISPATCH_BACK_KEY_TO_AUTOFILL = 23;
+ private static final int MSG_SYSTEM_KEY_PRESS = 24;
+ private static final int MSG_HANDLE_ALL_APPS = 25;
+ private static final int MSG_LAUNCH_ASSIST = 26;
+ private static final int MSG_LAUNCH_ASSIST_LONG_PRESS = 27;
+ private static final int MSG_POWER_VERY_LONG_PRESS = 28;
private static final int MSG_REQUEST_TRANSIENT_BARS_ARG_STATUS = 0;
private static final int MSG_REQUEST_TRANSIENT_BARS_ARG_NAVIGATION = 1;
@@ -881,15 +879,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
break;
case MSG_BACK_LONG_PRESS:
backLongPress();
- finishBackKeyPress();
break;
case MSG_DISPOSE_INPUT_CONSUMER:
disposeInputConsumer((InputConsumer) msg.obj);
break;
- case MSG_BACK_DELAYED_PRESS:
- backMultiPressAction(msg.arg1);
- finishBackKeyPress();
- break;
case MSG_ACCESSIBILITY_SHORTCUT:
accessibilityShortcutActivated();
break;
@@ -962,6 +955,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
resolver.registerContentObserver(Settings.Global.getUriFor(
Settings.Global.POLICY_CONTROL), false, this,
UserHandle.USER_ALL);
+ resolver.registerContentObserver(Settings.Global.getUriFor(
+ Settings.Global.EMULATE_DISPLAY_CUTOUT), false, this,
+ UserHandle.USER_ALL);
updateSettings();
}
@@ -1172,14 +1168,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// Reset back key state for long press
mBackKeyHandled = false;
- // Cancel multi-press detection timeout.
- if (hasPanicPressOnBackBehavior()) {
- if (mBackKeyPressCounter != 0
- && mBackKeyPressCounter < PANIC_PRESS_BACK_COUNT) {
- mHandler.removeMessages(MSG_BACK_DELAYED_PRESS);
- }
- }
-
if (hasLongPressOnBackBehavior()) {
Message msg = mHandler.obtainMessage(MSG_BACK_LONG_PRESS);
msg.setAsynchronous(true);
@@ -1193,21 +1181,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// Cache handled state
boolean handled = mBackKeyHandled;
- if (hasPanicPressOnBackBehavior()) {
- // Check for back key panic press
- ++mBackKeyPressCounter;
-
- final long eventTime = event.getDownTime();
-
- if (mBackKeyPressCounter <= PANIC_PRESS_BACK_COUNT) {
- // This could be a multi-press. Wait a little bit longer to confirm.
- Message msg = mHandler.obtainMessage(MSG_BACK_DELAYED_PRESS,
- mBackKeyPressCounter, 0, eventTime);
- msg.setAsynchronous(true);
- mHandler.sendMessageDelayed(msg, ViewConfiguration.getMultiPressTimeout());
- }
- }
-
// Reset back long press state
cancelPendingBackKeyAction();
@@ -1385,10 +1358,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- private void finishBackKeyPress() {
- mBackKeyPressCounter = 0;
- }
-
private void cancelPendingPowerKeyAction() {
if (!mPowerKeyHandled) {
mPowerKeyHandled = true;
@@ -1406,18 +1375,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- private void backMultiPressAction(int count) {
- if (count >= PANIC_PRESS_BACK_COUNT) {
- switch (mPanicPressOnBackBehavior) {
- case PANIC_PRESS_BACK_NOTHING:
- break;
- case PANIC_PRESS_BACK_HOME:
- launchHomeFromHotKey();
- break;
- }
- }
- }
-
private void powerPress(long eventTime, boolean interactive, int count) {
if (mScreenOnEarly && !mScreenOnFully) {
Slog.i(TAG, "Suppressed redundant power key press while "
@@ -1633,10 +1590,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return mLongPressOnBackBehavior != LONG_PRESS_BACK_NOTHING;
}
- private boolean hasPanicPressOnBackBehavior() {
- return mPanicPressOnBackBehavior != PANIC_PRESS_BACK_NOTHING;
- }
-
private void interceptScreenshotChord() {
if (mScreenshotChordEnabled
&& mScreenshotChordVolumeDownKeyTriggered && mScreenshotChordPowerKeyTriggered
@@ -2027,8 +1980,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mLongPressOnBackBehavior = mContext.getResources().getInteger(
com.android.internal.R.integer.config_longPressOnBackBehavior);
- mPanicPressOnBackBehavior = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_backPanicBehavior);
mShortPressOnPowerBehavior = mContext.getResources().getInteger(
com.android.internal.R.integer.config_shortPressOnPowerBehavior);
@@ -2175,14 +2126,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mWindowManagerInternal.registerAppTransitionListener(new AppTransitionListener() {
@Override
public int onAppTransitionStartingLocked(int transit, IBinder openToken,
- IBinder closeToken,
- Animation openAnimation, Animation closeAnimation) {
- return handleStartTransitionForKeyguardLw(transit, openAnimation);
+ IBinder closeToken, long duration, long statusBarAnimationStartTime,
+ long statusBarAnimationDuration) {
+ return handleStartTransitionForKeyguardLw(transit, duration);
}
@Override
public void onAppTransitionCancelledLocked(int transit) {
- handleStartTransitionForKeyguardLw(transit, null /* transit */);
+ handleStartTransitionForKeyguardLw(transit, 0 /* duration */);
}
});
mKeyguardDelegate = new KeyguardServiceDelegate(mContext,
@@ -2393,6 +2344,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (mImmersiveModeConfirmation != null) {
mImmersiveModeConfirmation.loadSetting(mCurrentUserId);
}
+ mEmulateDisplayCutout = Settings.Global.getInt(resolver,
+ Settings.Global.EMULATE_DISPLAY_CUTOUT,
+ Settings.Global.EMULATE_DISPLAY_CUTOUT_OFF)
+ != Settings.Global.EMULATE_DISPLAY_CUTOUT_OFF;
}
synchronized (mWindowManagerFuncs.getWindowManagerLock()) {
PolicyControl.reloadFromSetting(mContext);
@@ -2544,7 +2499,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// check if user has enabled this operation. SecurityException will be thrown if this app
// has not been allowed by the user
- final int mode = mAppOpsManager.checkOpNoThrow(outAppOp[0], callingUid, attrs.packageName);
+ final int mode = mAppOpsManager.noteOpNoThrow(outAppOp[0], callingUid, attrs.packageName);
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
case AppOpsManager.MODE_IGNORED:
@@ -4034,7 +3989,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- private int handleStartTransitionForKeyguardLw(int transit, @Nullable Animation anim) {
+ private int handleStartTransitionForKeyguardLw(int transit, long duration) {
if (mKeyguardOccludedChanged) {
if (DEBUG_KEYGUARD) Slog.d(TAG, "transition/occluded changed occluded="
+ mPendingKeyguardOccluded);
@@ -4045,13 +4000,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
if (AppTransition.isKeyguardGoingAwayTransit(transit)) {
if (DEBUG_KEYGUARD) Slog.d(TAG, "Starting keyguard exit animation");
- final long startTime = anim != null
- ? SystemClock.uptimeMillis() + anim.getStartOffset()
- : SystemClock.uptimeMillis();
- final long duration = anim != null
- ? anim.getDuration()
- : 0;
- startKeyguardExitAnimation(startTime, duration);
+ startKeyguardExitAnimation(SystemClock.uptimeMillis(), duration);
}
return 0;
}
@@ -4335,7 +4284,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// TODO: Should probably be moved into DisplayFrames.
public boolean getInsetHintLw(WindowManager.LayoutParams attrs, Rect taskBounds,
DisplayFrames displayFrames, Rect outContentInsets, Rect outStableInsets,
- Rect outOutsets) {
+ Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout) {
final int fl = PolicyControl.getWindowFlags(null, attrs);
final int sysuiVis = PolicyControl.getSystemUiVisibility(null, attrs);
final int systemUiVisibility = (sysuiVis | attrs.subtreeSystemUiVisibility);
@@ -4361,12 +4310,15 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if ((fl & (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR))
== (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) {
+ Rect frame;
int availRight, availBottom;
if (canHideNavigationBar() &&
(systemUiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0) {
+ frame = displayFrames.mUnrestricted;
availRight = displayFrames.mUnrestricted.right;
availBottom = displayFrames.mUnrestricted.bottom;
} else {
+ frame = displayFrames.mRestricted;
availRight = displayFrames.mRestricted.right;
availBottom = displayFrames.mRestricted.bottom;
}
@@ -4397,10 +4349,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
calculateRelevantTaskInsets(taskBounds, outStableInsets,
displayWidth, displayHeight);
}
+ outDisplayCutout.set(displayFrames.mDisplayCutout.calculateRelativeTo(frame));
return mForceShowSystemBars;
}
outContentInsets.setEmpty();
outStableInsets.setEmpty();
+ outDisplayCutout.set(DisplayCutout.NO_CUTOUT);
return mForceShowSystemBars;
}
@@ -4428,7 +4382,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
/** {@inheritDoc} */
@Override
public void beginLayoutLw(DisplayFrames displayFrames, int uiMode) {
- displayFrames.onBeginLayout();
+ displayFrames.onBeginLayout(mEmulateDisplayCutout, mStatusBarHeight);
// TODO(multi-display): This doesn't seem right...Maybe only apply to default display?
mSystemGestures.screenWidth = displayFrames.mUnrestricted.width();
mSystemGestures.screenHeight = displayFrames.mUnrestricted.height();
@@ -4498,6 +4452,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
layoutScreenDecorWindows(displayFrames, pf, df, dcf);
+
+ if (displayFrames.mDisplayCutoutSafe.top > displayFrames.mUnrestricted.top) {
+ // Make sure that the zone we're avoiding for the cutout is at least as tall as the
+ // status bar; otherwise fullscreen apps will end up cutting halfway into the status
+ // bar.
+ displayFrames.mDisplayCutoutSafe.top = Math.max(displayFrames.mDisplayCutoutSafe.top,
+ displayFrames.mStable.top);
+ }
}
private void layoutScreenDecorWindows(DisplayFrames displayFrames, Rect pf, Rect df, Rect dcf) {
@@ -4519,7 +4481,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
w.computeFrameLw(pf /* parentFrame */, df /* displayFrame */, df /* overlayFrame */,
df /* contentFrame */, df /* visibleFrame */, dcf /* decorFrame */,
- df /* stableFrame */, df /* outsetFrame */);
+ df /* stableFrame */, df /* outsetFrame */, displayFrames.mDisplayCutout);
final Rect frame = w.getFrameLw();
if (frame.left <= 0 && frame.top <= 0) {
@@ -4581,7 +4543,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// Let the status bar determine its size.
mStatusBar.computeFrameLw(pf /* parentFrame */, df /* displayFrame */,
vf /* overlayFrame */, vf /* contentFrame */, vf /* visibleFrame */,
- dcf /* decorFrame */, vf /* stableFrame */, vf /* outsetFrame */);
+ dcf /* decorFrame */, vf /* stableFrame */, vf /* outsetFrame */,
+ displayFrames.mDisplayCutout);
// For layout, the status bar is always at the top with our fixed height.
displayFrames.mStable.top = displayFrames.mUnrestricted.top + mStatusBarHeight;
@@ -4632,11 +4595,15 @@ public class PhoneWindowManager implements WindowManagerPolicy {
final Rect dockFrame = displayFrames.mDock;
mNavigationBarPosition = navigationBarPosition(displayWidth, displayHeight, rotation);
+ final Rect cutoutSafeUnrestricted = mTmpRect;
+ cutoutSafeUnrestricted.set(displayFrames.mUnrestricted);
+ cutoutSafeUnrestricted.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
+
if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
// It's a system nav bar or a portrait screen; nav bar goes on bottom.
- final int top = displayFrames.mUnrestricted.bottom
+ final int top = cutoutSafeUnrestricted.bottom
- getNavigationBarHeight(rotation, uiMode);
- mTmpNavigationFrame.set(0, top, displayWidth, displayFrames.mUnrestricted.bottom);
+ mTmpNavigationFrame.set(0, top, displayWidth, cutoutSafeUnrestricted.bottom);
displayFrames.mStable.bottom = displayFrames.mStableFullscreen.bottom = top;
if (transientNavBarShowing) {
mNavigationBarController.setBarShowingLw(true);
@@ -4657,9 +4624,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
} else if (mNavigationBarPosition == NAV_BAR_RIGHT) {
// Landscape screen; nav bar goes to the right.
- final int left = displayFrames.mUnrestricted.right
+ final int left = cutoutSafeUnrestricted.right
- getNavigationBarWidth(rotation, uiMode);
- mTmpNavigationFrame.set(left, 0, displayFrames.mUnrestricted.right, displayHeight);
+ mTmpNavigationFrame.set(left, 0, cutoutSafeUnrestricted.right, displayHeight);
displayFrames.mStable.right = displayFrames.mStableFullscreen.right = left;
if (transientNavBarShowing) {
mNavigationBarController.setBarShowingLw(true);
@@ -4680,9 +4647,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
} else if (mNavigationBarPosition == NAV_BAR_LEFT) {
// Seascape screen; nav bar goes to the left.
- final int right = displayFrames.mUnrestricted.left
+ final int right = cutoutSafeUnrestricted.left
+ getNavigationBarWidth(rotation, uiMode);
- mTmpNavigationFrame.set(displayFrames.mUnrestricted.left, 0, right, displayHeight);
+ mTmpNavigationFrame.set(cutoutSafeUnrestricted.left, 0, right, displayHeight);
displayFrames.mStable.left = displayFrames.mStableFullscreen.left = right;
if (transientNavBarShowing) {
mNavigationBarController.setBarShowingLw(true);
@@ -4712,7 +4679,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// And compute the final frame.
mNavigationBar.computeFrameLw(mTmpNavigationFrame, mTmpNavigationFrame,
mTmpNavigationFrame, mTmpNavigationFrame, mTmpNavigationFrame, dcf,
- mTmpNavigationFrame, mTmpNavigationFrame);
+ mTmpNavigationFrame, mTmpNavigationFrame, displayFrames.mDisplayCutout);
if (DEBUG_LAYOUT) Slog.i(TAG, "mNavigationBar frame: " + mTmpNavigationFrame);
return mNavigationBarController.checkHiddenLw();
}
@@ -4834,6 +4801,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
final int type = attrs.type;
final int fl = PolicyControl.getWindowFlags(win, attrs);
+ final long fl2 = attrs.flags2;
final int pfl = attrs.privateFlags;
final int sim = attrs.softInputMode;
final int requestedSysUiFl = PolicyControl.getSystemUiVisibility(win, null);
@@ -4854,6 +4822,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
final int adjust = sim & SOFT_INPUT_MASK_ADJUST;
+ final boolean requestedFullscreen = (fl & FLAG_FULLSCREEN) != 0
+ || (requestedSysUiFl & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0
+ || (requestedSysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0;
+
+ final boolean layoutInScreen = (fl & FLAG_LAYOUT_IN_SCREEN) == FLAG_LAYOUT_IN_SCREEN;
+ final boolean layoutInsetDecor = (fl & FLAG_LAYOUT_INSET_DECOR) == FLAG_LAYOUT_INSET_DECOR;
+ final boolean layoutInCutout = (fl2 & FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA) != 0;
+
sf.set(displayFrames.mStable);
if (type == TYPE_INPUT_METHOD) {
@@ -4863,7 +4839,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
df.set(displayFrames.mDock);
pf.set(displayFrames.mDock);
// IM dock windows layout below the nav bar...
- pf.bottom = df.bottom = of.bottom = displayFrames.mUnrestricted.bottom;
+ pf.bottom = df.bottom = of.bottom = Math.min(displayFrames.mUnrestricted.bottom,
+ displayFrames.mDisplayCutoutSafe.bottom);
// ...with content insets above the nav bar
cf.bottom = vf.bottom = displayFrames.mStable.bottom;
if (mStatusBar != null && mFocusedWindow == mStatusBar && canReceiveInput(mStatusBar)) {
@@ -4934,8 +4911,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- if ((fl & (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR))
- == (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) {
+ if (layoutInScreen && layoutInsetDecor) {
if (DEBUG_LAYOUT) Slog.v(TAG, "layoutWindowLw(" + attrs.getTitle()
+ "): IN_SCREEN, INSET_DECOR");
// This is the case for a normal activity window: we want it to cover all of the
@@ -5012,6 +4988,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// moving from a window that is not hiding the status bar to one that is.
cf.set(displayFrames.mRestricted);
}
+ if (requestedFullscreen && !layoutInCutout) {
+ pf.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
+ }
applyStableConstraints(sysUiFl, fl, cf, displayFrames);
if (adjust != SOFT_INPUT_ADJUST_NOTHING) {
vf.set(displayFrames.mCurrent);
@@ -5019,7 +4998,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
vf.set(cf);
}
}
- } else if ((fl & FLAG_LAYOUT_IN_SCREEN) != 0 || (sysUiFl
+ } else if (layoutInScreen || (sysUiFl
& (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)) != 0) {
if (DEBUG_LAYOUT) Slog.v(TAG, "layoutWindowLw(" + attrs.getTitle()
@@ -5097,6 +5076,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
of.set(displayFrames.mUnrestricted);
df.set(displayFrames.mUnrestricted);
pf.set(displayFrames.mUnrestricted);
+ if (requestedFullscreen && !layoutInCutout) {
+ pf.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
+ }
} else if ((sysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0) {
of.set(displayFrames.mRestricted);
df.set(displayFrames.mRestricted);
@@ -5167,9 +5149,27 @@ public class PhoneWindowManager implements WindowManagerPolicy {
vf.set(cf);
}
}
+ pf.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
}
}
+ // Ensure that windows that did not request to be laid out in the cutout don't get laid
+ // out there.
+ if (!layoutInCutout) {
+ final Rect displayCutoutSafeExceptMaybeTop = mTmpRect;
+ displayCutoutSafeExceptMaybeTop.set(displayFrames.mDisplayCutoutSafe);
+ if (layoutInScreen && layoutInsetDecor) {
+ // At the top we have the status bar, so apps that are
+ // LAYOUT_IN_SCREEN | LAYOUT_INSET_DECOR already expect that there's an inset
+ // there and we don't need to exclude the window from that area.
+ displayCutoutSafeExceptMaybeTop.top = Integer.MIN_VALUE;
+ }
+ pf.intersectUnchecked(displayCutoutSafeExceptMaybeTop);
+ }
+
+ // Content should never appear in the cutout.
+ cf.intersectUnchecked(displayFrames.mDisplayCutoutSafe);
+
// TYPE_SYSTEM_ERROR is above the NavigationBar so it can't be allowed to extend over it.
// Also, we don't allow windows in multi-window mode to extend out of the screen.
if ((fl & FLAG_LAYOUT_NO_LIMITS) != 0 && type != TYPE_SYSTEM_ERROR
@@ -5218,7 +5218,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
+ " sf=" + sf.toShortString()
+ " osf=" + (osf == null ? "null" : osf.toShortString()));
- win.computeFrameLw(pf, df, of, cf, vf, dcf, sf, osf);
+ win.computeFrameLw(pf, df, of, cf, vf, dcf, sf, osf, displayFrames.mDisplayCutout);
// Dock windows carve out the bottom of the screen, so normal windows
// can't appear underneath them.
@@ -7213,15 +7213,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
static long[] getLongIntArray(Resources r, int resid) {
- int[] ar = r.getIntArray(resid);
- if (ar == null) {
- return null;
- }
- long[] out = new long[ar.length];
- for (int i=0; i<ar.length; i++) {
- out[i] = ar[i];
- }
- return out;
+ return ArrayUtils.convertToLongArray(r.getIntArray(resid));
}
private void bindKeyguard() {
@@ -8247,9 +8239,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
pw.print("mLongPressOnBackBehavior=");
pw.println(longPressOnBackBehaviorToString(mLongPressOnBackBehavior));
pw.print(prefix);
- pw.print("mPanicPressOnBackBehavior=");
- pw.println(panicPressOnBackBehaviorToString(mPanicPressOnBackBehavior));
- pw.print(prefix);
pw.print("mLongPressOnHomeBehavior=");
pw.println(longPressOnHomeBehaviorToString(mLongPressOnHomeBehavior));
pw.print(prefix);
@@ -8449,17 +8438,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- private static String panicPressOnBackBehaviorToString(int behavior) {
- switch (behavior) {
- case PANIC_PRESS_BACK_NOTHING:
- return "PANIC_PRESS_BACK_NOTHING";
- case PANIC_PRESS_BACK_HOME:
- return "PANIC_PRESS_BACK_HOME";
- default:
- return Integer.toString(behavior);
- }
- }
-
private static String longPressOnHomeBehaviorToString(int behavior) {
switch (behavior) {
case LONG_PRESS_HOME_NOTHING:
diff --git a/com/android/server/policy/StatusBarController.java b/com/android/server/policy/StatusBarController.java
index af7e91c5..e6e4d7f9 100644
--- a/com/android/server/policy/StatusBarController.java
+++ b/com/android/server/policy/StatusBarController.java
@@ -37,8 +37,6 @@ import com.android.server.statusbar.StatusBarManagerInternal;
*/
public class StatusBarController extends BarController {
- private static final long TRANSITION_DURATION = 120L;
-
private final AppTransitionListener mAppTransitionListener
= new AppTransitionListener() {
@@ -57,17 +55,15 @@ public class StatusBarController extends BarController {
@Override
public int onAppTransitionStartingLocked(int transit, IBinder openToken,
- IBinder closeToken, final Animation openAnimation, final Animation closeAnimation) {
+ IBinder closeToken, long duration, long statusBarAnimationStartTime,
+ long statusBarAnimationDuration) {
mHandler.post(new Runnable() {
@Override
public void run() {
StatusBarManagerInternal statusbar = getStatusBarInternal();
if (statusbar != null) {
- long startTime = calculateStatusBarTransitionStartTime(openAnimation,
- closeAnimation);
- long duration = closeAnimation != null || openAnimation != null
- ? TRANSITION_DURATION : 0;
- statusbar.appTransitionStarting(startTime, duration);
+ statusbar.appTransitionStarting(statusBarAnimationStartTime,
+ statusBarAnimationDuration);
}
}
});
@@ -128,72 +124,4 @@ public class StatusBarController extends BarController {
public AppTransitionListener getAppTransitionListener() {
return mAppTransitionListener;
}
-
- /**
- * For a given app transition with {@code openAnimation} and {@code closeAnimation}, this
- * calculates the timings for the corresponding status bar transition.
- *
- * @return the desired start time of the status bar transition, in uptime millis
- */
- private static long calculateStatusBarTransitionStartTime(Animation openAnimation,
- Animation closeAnimation) {
- if (openAnimation != null && closeAnimation != null) {
- TranslateAnimation openTranslateAnimation = findTranslateAnimation(openAnimation);
- TranslateAnimation closeTranslateAnimation = findTranslateAnimation(closeAnimation);
- if (openTranslateAnimation != null) {
-
- // Some interpolators are extremely quickly mostly finished, but not completely. For
- // our purposes, we need to find the fraction for which ther interpolator is mostly
- // there, and use that value for the calculation.
- float t = findAlmostThereFraction(openTranslateAnimation.getInterpolator());
- return SystemClock.uptimeMillis()
- + openTranslateAnimation.getStartOffset()
- + (long)(openTranslateAnimation.getDuration()*t) - TRANSITION_DURATION;
- } else if (closeTranslateAnimation != null) {
- return SystemClock.uptimeMillis();
- } else {
- return SystemClock.uptimeMillis();
- }
- } else {
- return SystemClock.uptimeMillis();
- }
- }
-
- /**
- * Tries to find a {@link TranslateAnimation} inside the {@code animation}.
- *
- * @return the found animation, {@code null} otherwise
- */
- private static TranslateAnimation findTranslateAnimation(Animation animation) {
- if (animation instanceof TranslateAnimation) {
- return (TranslateAnimation) animation;
- } else if (animation instanceof AnimationSet) {
- AnimationSet set = (AnimationSet) animation;
- for (int i = 0; i < set.getAnimations().size(); i++) {
- Animation a = set.getAnimations().get(i);
- if (a instanceof TranslateAnimation) {
- return (TranslateAnimation) a;
- }
- }
- }
- return null;
- }
-
- /**
- * Binary searches for a {@code t} such that there exists a {@code -0.01 < eps < 0.01} for which
- * {@code interpolator(t + eps) > 0.99}.
- */
- private static float findAlmostThereFraction(Interpolator interpolator) {
- float val = 0.5f;
- float adj = 0.25f;
- while (adj >= 0.01f) {
- if (interpolator.getInterpolation(val) < 0.99f) {
- val += adj;
- } else {
- val -= adj;
- }
- adj /= 2;
- }
- return val;
- }
}
diff --git a/com/android/server/policy/WindowManagerPolicy.java b/com/android/server/policy/WindowManagerPolicy.java
index 5f067d49..cfe40887 100644
--- a/com/android/server/policy/WindowManagerPolicy.java
+++ b/com/android/server/policy/WindowManagerPolicy.java
@@ -77,6 +77,7 @@ import android.os.RemoteException;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
+import android.view.DisplayCutout;
import android.view.IApplicationToken;
import android.view.IWindowManager;
import android.view.InputEventReceiver;
@@ -210,10 +211,11 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
* @param stableFrame The frame around which stable system decoration is positioned.
* @param outsetFrame The frame that includes areas that aren't part of the surface but we
* want to treat them as such.
+ * @param displayCutout the display displayCutout
*/
public void computeFrameLw(Rect parentFrame, Rect displayFrame,
Rect overlayFrame, Rect contentFrame, Rect visibleFrame, Rect decorFrame,
- Rect stableFrame, @Nullable Rect outsetFrame);
+ Rect stableFrame, @Nullable Rect outsetFrame, DisplayCutout displayCutout);
/**
* Retrieve the current frame of the window that has been assigned by
@@ -1144,12 +1146,13 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
* @param outStableInsets The areas covered by stable system windows irrespective of their
* current visibility. Expressed as positive insets.
* @param outOutsets The areas that are not real display, but we would like to treat as such.
+ * @param outDisplayCutout The area that has been cut away from the display.
* @return Whether to always consume the navigation bar.
* See {@link #isNavBarForcedShownLw(WindowState)}.
*/
default boolean getInsetHintLw(WindowManager.LayoutParams attrs, Rect taskBounds,
DisplayFrames displayFrames, Rect outContentInsets, Rect outStableInsets,
- Rect outOutsets) {
+ Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout) {
return false;
}
@@ -1359,7 +1362,7 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
/**
* inKeyguardRestrictedKeyInputMode
*
- * if keyguard screen is showing or in restricted key input mode (i.e. in
+ * 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.
*
diff --git a/com/android/server/power/BatterySaverPolicy.java b/com/android/server/power/BatterySaverPolicy.java
index 7c234f96..6f005a35 100644
--- a/com/android/server/power/BatterySaverPolicy.java
+++ b/com/android/server/power/BatterySaverPolicy.java
@@ -49,11 +49,21 @@ public class BatterySaverPolicy extends ContentObserver {
public static final boolean DEBUG = false; // DO NOT SUBMIT WITH TRUE.
- // Value of batterySaverGpsMode such that GPS isn't affected by battery saver mode.
+ /** Value of batterySaverGpsMode such that GPS isn't affected by battery saver mode. */
public static final int GPS_MODE_NO_CHANGE = 0;
- // Value of batterySaverGpsMode such that GPS is disabled when battery saver mode
- // is enabled and the screen is off.
+
+ /**
+ * Value of batterySaverGpsMode such that GPS is disabled when battery saver mode
+ * is enabled and the screen is off.
+ */
public static final int GPS_MODE_DISABLED_WHEN_SCREEN_OFF = 1;
+
+ /**
+ * Value of batterySaverGpsMode such that location should be disabled altogether
+ * when battery saver mode is enabled and the screen is off.
+ */
+ public static final int GPS_MODE_ALL_DISABLED_WHEN_SCREEN_OFF = 2;
+
// Secure setting for GPS behavior when battery saver mode is on.
public static final String SECURE_KEY_GPS_MODE = "batterySaverGpsMode";
@@ -87,6 +97,12 @@ public class BatterySaverPolicy extends ContentObserver {
private String mDeviceSpecificSettingsSource; // For dump() only.
/**
+ * A short string describing which battery saver is now enabled, which we dump in the eventlog.
+ */
+ @GuardedBy("mLock")
+ private String mEventLogKeys;
+
+ /**
* {@code true} if vibration is disabled in battery saver mode.
*
* @see Settings.Global#BATTERY_SAVER_CONSTANTS
@@ -323,12 +339,12 @@ public class BatterySaverPolicy extends ContentObserver {
}
mVibrationDisabled = parser.getBoolean(KEY_VIBRATION_DISABLED, true);
- mAnimationDisabled = parser.getBoolean(KEY_ANIMATION_DISABLED, true);
+ mAnimationDisabled = parser.getBoolean(KEY_ANIMATION_DISABLED, false);
mSoundTriggerDisabled = parser.getBoolean(KEY_SOUNDTRIGGER_DISABLED, true);
mFullBackupDeferred = parser.getBoolean(KEY_FULLBACKUP_DEFERRED, true);
mKeyValueBackupDeferred = parser.getBoolean(KEY_KEYVALUE_DEFERRED, true);
mFireWallDisabled = parser.getBoolean(KEY_FIREWALL_DISABLED, false);
- mAdjustBrightnessDisabled = parser.getBoolean(KEY_ADJUST_BRIGHTNESS_DISABLED, false);
+ mAdjustBrightnessDisabled = parser.getBoolean(KEY_ADJUST_BRIGHTNESS_DISABLED, true);
mAdjustBrightnessFactor = parser.getFloat(KEY_ADJUST_BRIGHTNESS_FACTOR, 0.5f);
mDataSaverDisabled = parser.getBoolean(KEY_DATASAVER_DISABLED, true);
mLaunchBoostDisabled = parser.getBoolean(KEY_LAUNCH_BOOST_DISABLED, true);
@@ -338,7 +354,7 @@ public class BatterySaverPolicy extends ContentObserver {
// Get default value from Settings.Secure
final int defaultGpsMode = Settings.Secure.getInt(mContentResolver, SECURE_KEY_GPS_MODE,
- GPS_MODE_NO_CHANGE);
+ GPS_MODE_ALL_DISABLED_WHEN_SCREEN_OFF);
mGpsMode = parser.getInt(KEY_GPS_MODE, defaultGpsMode);
// Non-device-specific parameters.
@@ -354,6 +370,27 @@ public class BatterySaverPolicy extends ContentObserver {
mFilesForNoninteractive = (new CpuFrequencies()).parseString(
parser.getString(KEY_CPU_FREQ_NONINTERACTIVE, "")).toSysFileMap();
+
+ final StringBuilder sb = new StringBuilder();
+
+ if (mForceAllAppsStandby) sb.append("A");
+ if (mForceBackgroundCheck) sb.append("B");
+
+ if (mVibrationDisabled) sb.append("v");
+ if (mAnimationDisabled) sb.append("a");
+ if (mSoundTriggerDisabled) sb.append("s");
+ if (mFullBackupDeferred) sb.append("F");
+ if (mKeyValueBackupDeferred) sb.append("K");
+ if (!mFireWallDisabled) sb.append("f");
+ if (!mDataSaverDisabled) sb.append("d");
+ if (!mAdjustBrightnessDisabled) sb.append("b");
+
+ if (mLaunchBoostDisabled) sb.append("l");
+ if (mOptionalSensorsDisabled) sb.append("S");
+
+ sb.append(mGpsMode);
+
+ mEventLogKeys = sb.toString();
}
/**
@@ -419,6 +456,12 @@ public class BatterySaverPolicy extends ContentObserver {
}
}
+ public int getGpsMode() {
+ synchronized (mLock) {
+ return mGpsMode;
+ }
+ }
+
public ArrayMap<String, String> getFileValues(boolean interactive) {
synchronized (mLock) {
return interactive ? mFilesForInteractive : mFilesForNoninteractive;
@@ -431,6 +474,12 @@ public class BatterySaverPolicy extends ContentObserver {
}
}
+ public String toEventLogString() {
+ synchronized (mLock) {
+ return mEventLogKeys;
+ }
+ }
+
public void dump(PrintWriter pw) {
synchronized (mLock) {
pw.println();
diff --git a/com/android/server/power/Notifier.java b/com/android/server/power/Notifier.java
index 8ee26f29..e5a23ea3 100644
--- a/com/android/server/power/Notifier.java
+++ b/com/android/server/power/Notifier.java
@@ -16,6 +16,7 @@
package com.android.server.power;
+import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
@@ -27,6 +28,7 @@ import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.policy.WindowManagerPolicy;
+import android.app.trust.TrustManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -82,6 +84,7 @@ final class Notifier {
private static final int MSG_BROADCAST = 2;
private static final int MSG_WIRELESS_CHARGING_STARTED = 3;
private static final int MSG_SCREEN_BRIGHTNESS_BOOST_CHANGED = 4;
+ private static final int MSG_PROFILE_TIMED_OUT = 5;
private final Object mLock = new Object();
@@ -93,6 +96,7 @@ final class Notifier {
private final ActivityManagerInternal mActivityManagerInternal;
private final InputManagerInternal mInputManagerInternal;
private final InputMethodManagerInternal mInputMethodManagerInternal;
+ private final TrustManager mTrustManager;
private final NotifierHandler mHandler;
private final Intent mScreenOnIntent;
@@ -138,6 +142,7 @@ final class Notifier {
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class);
+ mTrustManager = mContext.getSystemService(TrustManager.class);
mHandler = new NotifierHandler(looper);
mScreenOnIntent = new Intent(Intent.ACTION_SCREEN_ON);
@@ -559,6 +564,16 @@ final class Notifier {
mHandler.sendMessage(msg);
}
+ /**
+ * Called when profile screen lock timeout has expired.
+ */
+ public void onProfileTimeout(@UserIdInt int userId) {
+ final Message msg = mHandler.obtainMessage(MSG_PROFILE_TIMED_OUT);
+ msg.setAsynchronous(true);
+ msg.arg1 = userId;
+ mHandler.sendMessage(msg);
+ }
+
private void updatePendingBroadcastLocked() {
if (!mBroadcastInProgress
&& mPendingInteractiveState != INTERACTIVE_STATE_UNKNOWN
@@ -710,28 +725,33 @@ final class Notifier {
mSuspendBlocker.release();
}
+ private void lockProfile(@UserIdInt int userId) {
+ mTrustManager.setDeviceLockedForUser(userId, true /*locked*/);
+ }
+
private final class NotifierHandler extends Handler {
+
public NotifierHandler(Looper looper) {
super(looper, null, true /*async*/);
}
-
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_USER_ACTIVITY:
sendUserActivity();
break;
-
case MSG_BROADCAST:
sendNextBroadcast();
break;
-
case MSG_WIRELESS_CHARGING_STARTED:
playWirelessChargingStartedSound();
break;
case MSG_SCREEN_BRIGHTNESS_BOOST_CHANGED:
sendBrightnessBoostChangedBroadcast();
break;
+ case MSG_PROFILE_TIMED_OUT:
+ lockProfile(msg.arg1);
+ break;
}
}
}
diff --git a/com/android/server/power/PowerManagerService.java b/com/android/server/power/PowerManagerService.java
index 7f1a534c..02c8f681 100644
--- a/com/android/server/power/PowerManagerService.java
+++ b/com/android/server/power/PowerManagerService.java
@@ -16,9 +16,10 @@
package com.android.server.power;
-import android.Manifest;
import android.annotation.IntDef;
+import android.annotation.UserIdInt;
import android.app.ActivityManager;
+import android.app.SynchronousUserSwitchObserver;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -410,12 +411,12 @@ public final class PowerManagerService extends SystemService
private boolean mDozeAfterScreenOffConfig;
// The minimum screen off timeout, in milliseconds.
- private int mMinimumScreenOffTimeoutConfig;
+ private long mMinimumScreenOffTimeoutConfig;
// The screen dim duration, in milliseconds.
// This is subtracted from the end of the screen off timeout so the
// minimum screen off timeout should be longer than this.
- private int mMaximumScreenDimDurationConfig;
+ private long mMaximumScreenDimDurationConfig;
// The maximum screen dim time expressed as a ratio relative to the screen
// off timeout. If the screen off timeout is very short then we want the
@@ -427,14 +428,14 @@ public final class PowerManagerService extends SystemService
private boolean mSupportsDoubleTapWakeConfig;
// The screen off timeout setting value in milliseconds.
- private int mScreenOffTimeoutSetting;
+ private long mScreenOffTimeoutSetting;
// The sleep timeout setting value in milliseconds.
- private int mSleepTimeoutSetting;
+ private long mSleepTimeoutSetting;
// The maximum allowable screen off timeout according to the device
// administration policy. Overrides other settings.
- private int mMaximumScreenOffTimeoutFromDeviceAdmin = Integer.MAX_VALUE;
+ private long mMaximumScreenOffTimeoutFromDeviceAdmin = Long.MAX_VALUE;
// The stay on while plugged in setting.
// A bitfield of battery conditions under which to make the screen stay on.
@@ -555,6 +556,46 @@ public final class PowerManagerService extends SystemService
// True if we are currently in VR Mode.
private boolean mIsVrModeEnabled;
+ private final class ForegroundProfileObserver extends SynchronousUserSwitchObserver {
+ @Override
+ public void onUserSwitching(int newUserId) throws RemoteException {}
+
+ @Override
+ public void onForegroundProfileSwitch(@UserIdInt int newProfileId) throws RemoteException {
+ final long now = SystemClock.uptimeMillis();
+ synchronized(mLock) {
+ mForegroundProfile = newProfileId;
+ maybeUpdateForegroundProfileLastActivityLocked(now);
+ }
+ }
+ }
+
+ // User id corresponding to activity the user is currently interacting with.
+ private @UserIdInt int mForegroundProfile;
+
+ // Per-profile state to track when a profile should be locked.
+ private final SparseArray<ProfilePowerState> mProfilePowerState = new SparseArray<>();
+
+ private static final class ProfilePowerState {
+ // Profile user id.
+ final @UserIdInt int mUserId;
+ // Maximum time to lock set by admin.
+ long mScreenOffTimeout;
+ // Like top-level mWakeLockSummary, but only for wake locks that affect current profile.
+ int mWakeLockSummary;
+ // Last user activity that happened in an app running in the profile.
+ long mLastUserActivityTime;
+ // Whether profile has been locked last time it timed out.
+ boolean mLockingNotified;
+
+ public ProfilePowerState(@UserIdInt int userId, long screenOffTimeout) {
+ mUserId = userId;
+ mScreenOffTimeout = screenOffTimeout;
+ // Not accurate but at least won't cause immediate locking of the profile.
+ mLastUserActivityTime = SystemClock.uptimeMillis();
+ }
+ }
+
/**
* All times are in milliseconds. These constants are kept synchronized with the system
* global Settings. Any access to this class or its fields should be done while
@@ -752,6 +793,12 @@ public final class PowerManagerService extends SystemService
mDisplayManagerInternal.initPowerManagement(
mDisplayPowerCallbacks, mHandler, sensorManager);
+ try {
+ final ForegroundProfileObserver observer = new ForegroundProfileObserver();
+ ActivityManager.getService().registerUserSwitchObserver(observer, TAG);
+ } catch (RemoteException e) {
+ // Shouldn't happen since in-process.
+ }
// Go.
readConfigurationLocked();
@@ -1333,6 +1380,8 @@ public final class PowerManagerService extends SystemService
return false;
}
+ maybeUpdateForegroundProfileLastActivityLocked(eventTime);
+
if ((flags & PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS) != 0) {
if (eventTime > mLastUserActivityTimeNoChangeLights
&& eventTime > mLastUserActivityTime) {
@@ -1360,6 +1409,13 @@ public final class PowerManagerService extends SystemService
return false;
}
+ private void maybeUpdateForegroundProfileLastActivityLocked(long eventTime) {
+ final ProfilePowerState profile = mProfilePowerState.get(mForegroundProfile);
+ if (profile != null && eventTime > profile.mLastUserActivityTime) {
+ profile.mLastUserActivityTime = eventTime;
+ }
+ }
+
private void wakeUpInternal(long eventTime, String reason, int uid, String opPackageName,
int opUid) {
synchronized (mLock) {
@@ -1648,16 +1704,19 @@ public final class PowerManagerService extends SystemService
}
}
- // Phase 2: Update display power state.
- boolean displayBecameReady = updateDisplayPowerStateLocked(dirtyPhase2);
+ // Phase 2: Lock profiles that became inactive/not kept awake.
+ updateProfilesLocked(now);
- // Phase 3: Update dream state (depends on display ready signal).
+ // Phase 3: Update display power state.
+ final boolean displayBecameReady = updateDisplayPowerStateLocked(dirtyPhase2);
+
+ // Phase 4: Update dream state (depends on display ready signal).
updateDreamLocked(dirtyPhase2, displayBecameReady);
- // Phase 4: Send notifications, if needed.
+ // Phase 5: Send notifications, if needed.
finishWakefulnessChangeIfNeededLocked();
- // Phase 5: Update suspend blocker.
+ // Phase 6: Update suspend blocker.
// Because we might release the last suspend blocker here, we need to make sure
// we finished everything else first!
updateSuspendBlockerLocked();
@@ -1667,6 +1726,29 @@ public final class PowerManagerService extends SystemService
}
/**
+ * Check profile timeouts and notify profiles that should be locked.
+ */
+ private void updateProfilesLocked(long now) {
+ final int numProfiles = mProfilePowerState.size();
+ for (int i = 0; i < numProfiles; i++) {
+ final ProfilePowerState profile = mProfilePowerState.valueAt(i);
+ if (isProfileBeingKeptAwakeLocked(profile, now)) {
+ profile.mLockingNotified = false;
+ } else if (!profile.mLockingNotified) {
+ profile.mLockingNotified = true;
+ mNotifier.onProfileTimeout(profile.mUserId);
+ }
+ }
+ }
+
+ private boolean isProfileBeingKeptAwakeLocked(ProfilePowerState profile, long now) {
+ return (profile.mLastUserActivityTime + profile.mScreenOffTimeout > now)
+ || (profile.mWakeLockSummary & WAKE_LOCK_STAY_AWAKE) != 0
+ || (mProximityPositive &&
+ (profile.mWakeLockSummary & WAKE_LOCK_PROXIMITY_SCREEN_OFF) != 0);
+ }
+
+ /**
* Updates the value of mIsPowered.
* Sets DIRTY_IS_POWERED if a change occurred.
*/
@@ -1800,60 +1882,28 @@ public final class PowerManagerService extends SystemService
if ((dirty & (DIRTY_WAKE_LOCKS | DIRTY_WAKEFULNESS)) != 0) {
mWakeLockSummary = 0;
+ final int numProfiles = mProfilePowerState.size();
+ for (int i = 0; i < numProfiles; i++) {
+ mProfilePowerState.valueAt(i).mWakeLockSummary = 0;
+ }
+
final int numWakeLocks = mWakeLocks.size();
for (int i = 0; i < numWakeLocks; i++) {
final WakeLock wakeLock = mWakeLocks.get(i);
- switch (wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK) {
- case PowerManager.PARTIAL_WAKE_LOCK:
- if (!wakeLock.mDisabled) {
- // We only respect this if the wake lock is not disabled.
- mWakeLockSummary |= WAKE_LOCK_CPU;
- }
- break;
- case PowerManager.FULL_WAKE_LOCK:
- mWakeLockSummary |= WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_BUTTON_BRIGHT;
- break;
- case PowerManager.SCREEN_BRIGHT_WAKE_LOCK:
- mWakeLockSummary |= WAKE_LOCK_SCREEN_BRIGHT;
- break;
- case PowerManager.SCREEN_DIM_WAKE_LOCK:
- mWakeLockSummary |= WAKE_LOCK_SCREEN_DIM;
- break;
- case PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK:
- mWakeLockSummary |= WAKE_LOCK_PROXIMITY_SCREEN_OFF;
- break;
- case PowerManager.DOZE_WAKE_LOCK:
- mWakeLockSummary |= WAKE_LOCK_DOZE;
- break;
- case PowerManager.DRAW_WAKE_LOCK:
- mWakeLockSummary |= WAKE_LOCK_DRAW;
- break;
- }
- }
-
- // Cancel wake locks that make no sense based on the current state.
- if (mWakefulness != WAKEFULNESS_DOZING) {
- mWakeLockSummary &= ~(WAKE_LOCK_DOZE | WAKE_LOCK_DRAW);
- }
- if (mWakefulness == WAKEFULNESS_ASLEEP
- || (mWakeLockSummary & WAKE_LOCK_DOZE) != 0) {
- mWakeLockSummary &= ~(WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_SCREEN_DIM
- | WAKE_LOCK_BUTTON_BRIGHT);
- if (mWakefulness == WAKEFULNESS_ASLEEP) {
- mWakeLockSummary &= ~WAKE_LOCK_PROXIMITY_SCREEN_OFF;
+ final int wakeLockFlags = getWakeLockSummaryFlags(wakeLock);
+ mWakeLockSummary |= wakeLockFlags;
+ for (int j = 0; j < numProfiles; j++) {
+ final ProfilePowerState profile = mProfilePowerState.valueAt(j);
+ if (wakeLockAffectsUser(wakeLock, profile.mUserId)) {
+ profile.mWakeLockSummary |= wakeLockFlags;
+ }
}
}
- // Infer implied wake locks where necessary based on the current state.
- if ((mWakeLockSummary & (WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_SCREEN_DIM)) != 0) {
- if (mWakefulness == WAKEFULNESS_AWAKE) {
- mWakeLockSummary |= WAKE_LOCK_CPU | WAKE_LOCK_STAY_AWAKE;
- } else if (mWakefulness == WAKEFULNESS_DREAMING) {
- mWakeLockSummary |= WAKE_LOCK_CPU;
- }
- }
- if ((mWakeLockSummary & WAKE_LOCK_DRAW) != 0) {
- mWakeLockSummary |= WAKE_LOCK_CPU;
+ mWakeLockSummary = adjustWakeLockSummaryLocked(mWakeLockSummary);
+ for (int i = 0; i < numProfiles; i++) {
+ final ProfilePowerState profile = mProfilePowerState.valueAt(i);
+ profile.mWakeLockSummary = adjustWakeLockSummaryLocked(profile.mWakeLockSummary);
}
if (DEBUG_SPEW) {
@@ -1864,6 +1914,72 @@ public final class PowerManagerService extends SystemService
}
}
+ private int adjustWakeLockSummaryLocked(int wakeLockSummary) {
+ // Cancel wake locks that make no sense based on the current state.
+ if (mWakefulness != WAKEFULNESS_DOZING) {
+ wakeLockSummary &= ~(WAKE_LOCK_DOZE | WAKE_LOCK_DRAW);
+ }
+ if (mWakefulness == WAKEFULNESS_ASLEEP
+ || (wakeLockSummary & WAKE_LOCK_DOZE) != 0) {
+ wakeLockSummary &= ~(WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_SCREEN_DIM
+ | WAKE_LOCK_BUTTON_BRIGHT);
+ if (mWakefulness == WAKEFULNESS_ASLEEP) {
+ wakeLockSummary &= ~WAKE_LOCK_PROXIMITY_SCREEN_OFF;
+ }
+ }
+
+ // Infer implied wake locks where necessary based on the current state.
+ if ((wakeLockSummary & (WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_SCREEN_DIM)) != 0) {
+ if (mWakefulness == WAKEFULNESS_AWAKE) {
+ wakeLockSummary |= WAKE_LOCK_CPU | WAKE_LOCK_STAY_AWAKE;
+ } else if (mWakefulness == WAKEFULNESS_DREAMING) {
+ wakeLockSummary |= WAKE_LOCK_CPU;
+ }
+ }
+ if ((wakeLockSummary & WAKE_LOCK_DRAW) != 0) {
+ wakeLockSummary |= WAKE_LOCK_CPU;
+ }
+
+ return wakeLockSummary;
+ }
+
+ /** Get wake lock summary flags that correspond to the given wake lock. */
+ private int getWakeLockSummaryFlags(WakeLock wakeLock) {
+ switch (wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK) {
+ case PowerManager.PARTIAL_WAKE_LOCK:
+ if (!wakeLock.mDisabled) {
+ // We only respect this if the wake lock is not disabled.
+ return WAKE_LOCK_CPU;
+ }
+ break;
+ case PowerManager.FULL_WAKE_LOCK:
+ return WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_BUTTON_BRIGHT;
+ case PowerManager.SCREEN_BRIGHT_WAKE_LOCK:
+ return WAKE_LOCK_SCREEN_BRIGHT;
+ case PowerManager.SCREEN_DIM_WAKE_LOCK:
+ return WAKE_LOCK_SCREEN_DIM;
+ case PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK:
+ return WAKE_LOCK_PROXIMITY_SCREEN_OFF;
+ case PowerManager.DOZE_WAKE_LOCK:
+ return WAKE_LOCK_DOZE;
+ case PowerManager.DRAW_WAKE_LOCK:
+ return WAKE_LOCK_DRAW;
+ }
+ return 0;
+ }
+
+ private boolean wakeLockAffectsUser(WakeLock wakeLock, @UserIdInt int userId) {
+ if (wakeLock.mWorkSource != null) {
+ for (int k = 0; k < wakeLock.mWorkSource.size(); k++) {
+ final int uid = wakeLock.mWorkSource.get(k);
+ if (userId == UserHandle.getUserId(uid)) {
+ return true;
+ }
+ }
+ }
+ return userId == UserHandle.getUserId(wakeLock.mOwnerUid);
+ }
+
void checkForLongWakeLocks() {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
@@ -1917,10 +2033,11 @@ public final class PowerManagerService extends SystemService
if (mWakefulness == WAKEFULNESS_AWAKE
|| mWakefulness == WAKEFULNESS_DREAMING
|| mWakefulness == WAKEFULNESS_DOZING) {
- final int sleepTimeout = getSleepTimeoutLocked();
- final int screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout);
- final int screenDimDuration = getScreenDimDurationLocked(screenOffTimeout);
+ final long sleepTimeout = getSleepTimeoutLocked();
+ final long screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout);
+ final long screenDimDuration = getScreenDimDurationLocked(screenOffTimeout);
final boolean userInactiveOverride = mUserInactiveOverrideFromWindowManager;
+ final long nextProfileTimeout = getNextProfileTimeoutLocked(now);
mUserActivitySummary = 0;
if (mLastUserActivityTime >= mLastWakeTime) {
@@ -1977,10 +2094,12 @@ public final class PowerManagerService extends SystemService
nextTimeout = -1;
}
+ if (nextProfileTimeout > 0) {
+ nextTimeout = Math.min(nextTimeout, nextProfileTimeout);
+ }
+
if (mUserActivitySummary != 0 && nextTimeout >= 0) {
- Message msg = mHandler.obtainMessage(MSG_USER_ACTIVITY_TIMEOUT);
- msg.setAsynchronous(true);
- mHandler.sendMessageAtTime(msg, nextTimeout);
+ scheduleUserInactivityTimeout(nextTimeout);
}
} else {
mUserActivitySummary = 0;
@@ -1995,6 +2114,28 @@ public final class PowerManagerService extends SystemService
}
}
+ private void scheduleUserInactivityTimeout(long timeMs) {
+ final Message msg = mHandler.obtainMessage(MSG_USER_ACTIVITY_TIMEOUT);
+ msg.setAsynchronous(true);
+ mHandler.sendMessageAtTime(msg, timeMs);
+ }
+
+ /**
+ * Finds the next profile timeout time or returns -1 if there are no profiles to be locked.
+ */
+ private long getNextProfileTimeoutLocked(long now) {
+ long nextTimeout = -1;
+ final int numProfiles = mProfilePowerState.size();
+ for (int i = 0; i < numProfiles; i++) {
+ final ProfilePowerState profile = mProfilePowerState.valueAt(i);
+ final long timeout = profile.mLastUserActivityTime + profile.mScreenOffTimeout;
+ if (timeout > now && (nextTimeout == -1 || timeout < nextTimeout)) {
+ nextTimeout = timeout;
+ }
+ }
+ return nextTimeout;
+ }
+
/**
* Called when a user activity timeout has occurred.
* Simply indicates that something about user activity has changed so that the new
@@ -2014,21 +2155,21 @@ public final class PowerManagerService extends SystemService
}
}
- private int getSleepTimeoutLocked() {
- int timeout = mSleepTimeoutSetting;
+ private long getSleepTimeoutLocked() {
+ final long timeout = mSleepTimeoutSetting;
if (timeout <= 0) {
return -1;
}
return Math.max(timeout, mMinimumScreenOffTimeoutConfig);
}
- private int getScreenOffTimeoutLocked(int sleepTimeout) {
- int timeout = mScreenOffTimeoutSetting;
+ private long getScreenOffTimeoutLocked(long sleepTimeout) {
+ long timeout = mScreenOffTimeoutSetting;
if (isMaximumScreenOffTimeoutFromDeviceAdminEnforcedLocked()) {
timeout = Math.min(timeout, mMaximumScreenOffTimeoutFromDeviceAdmin);
}
if (mUserActivityTimeoutOverrideFromWindowManager >= 0) {
- timeout = (int)Math.min(timeout, mUserActivityTimeoutOverrideFromWindowManager);
+ timeout = Math.min(timeout, mUserActivityTimeoutOverrideFromWindowManager);
}
if (sleepTimeout >= 0) {
timeout = Math.min(timeout, sleepTimeout);
@@ -2036,9 +2177,9 @@ public final class PowerManagerService extends SystemService
return Math.max(timeout, mMinimumScreenOffTimeoutConfig);
}
- private int getScreenDimDurationLocked(int screenOffTimeout) {
+ private long getScreenDimDurationLocked(long screenOffTimeout) {
return Math.min(mMaximumScreenDimDurationConfig,
- (int)(screenOffTimeout * mMaximumScreenDimRatioConfig));
+ (long)(screenOffTimeout * mMaximumScreenDimRatioConfig));
}
/**
@@ -2781,9 +2922,27 @@ public final class PowerManagerService extends SystemService
Settings.Global.STAY_ON_WHILE_PLUGGED_IN, val);
}
- void setMaximumScreenOffTimeoutFromDeviceAdminInternal(int timeMs) {
+ void setMaximumScreenOffTimeoutFromDeviceAdminInternal(@UserIdInt int userId, long timeMs) {
+ if (userId < 0) {
+ Slog.wtf(TAG, "Attempt to set screen off timeout for invalid user: " + userId);
+ return;
+ }
synchronized (mLock) {
- mMaximumScreenOffTimeoutFromDeviceAdmin = timeMs;
+ // System-wide timeout
+ if (userId == UserHandle.USER_SYSTEM) {
+ mMaximumScreenOffTimeoutFromDeviceAdmin = timeMs;
+ } else if (timeMs == Long.MAX_VALUE || timeMs == 0) {
+ mProfilePowerState.delete(userId);
+ } else {
+ final ProfilePowerState profile = mProfilePowerState.get(userId);
+ if (profile != null) {
+ profile.mScreenOffTimeout = timeMs;
+ } else {
+ mProfilePowerState.put(userId, new ProfilePowerState(userId, timeMs));
+ // We need to recalculate wake locks for the new profile state.
+ mDirty |= DIRTY_WAKE_LOCKS;
+ }
+ }
mDirty |= DIRTY_SETTINGS;
updatePowerStateLocked();
}
@@ -2981,7 +3140,7 @@ public final class PowerManagerService extends SystemService
private boolean isMaximumScreenOffTimeoutFromDeviceAdminEnforcedLocked() {
return mMaximumScreenOffTimeoutFromDeviceAdmin >= 0
- && mMaximumScreenOffTimeoutFromDeviceAdmin < Integer.MAX_VALUE;
+ && mMaximumScreenOffTimeoutFromDeviceAdmin < Long.MAX_VALUE;
}
private void setAttentionLightInternal(boolean on, int color) {
@@ -3325,10 +3484,11 @@ public final class PowerManagerService extends SystemService
pw.println(" mScreenBrightnessForVrSetting=" + mScreenBrightnessForVrSetting);
pw.println(" mDoubleTapWakeEnabled=" + mDoubleTapWakeEnabled);
pw.println(" mIsVrModeEnabled=" + mIsVrModeEnabled);
+ pw.println(" mForegroundProfile=" + mForegroundProfile);
- final int sleepTimeout = getSleepTimeoutLocked();
- final int screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout);
- final int screenDimDuration = getScreenDimDurationLocked(screenOffTimeout);
+ final long sleepTimeout = getSleepTimeoutLocked();
+ final long screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout);
+ final long screenDimDuration = getScreenDimDurationLocked(screenOffTimeout);
pw.println();
pw.println("Sleep timeout: " + sleepTimeout + " ms");
pw.println("Screen off timeout: " + screenOffTimeout + " ms");
@@ -3373,6 +3533,23 @@ public final class PowerManagerService extends SystemService
mBatterySaverPolicy.dump(pw);
+ pw.println();
+ final int numProfiles = mProfilePowerState.size();
+ pw.println("Profile power states: size=" + numProfiles);
+ for (int i = 0; i < numProfiles; i++) {
+ final ProfilePowerState profile = mProfilePowerState.valueAt(i);
+ pw.print(" mUserId=");
+ pw.print(profile.mUserId);
+ pw.print(" mScreenOffTimeout=");
+ pw.print(profile.mScreenOffTimeout);
+ pw.print(" mWakeLockSummary=");
+ pw.print(profile.mWakeLockSummary);
+ pw.print(" mLastUserActivityTime=");
+ pw.print(profile.mLastUserActivityTime);
+ pw.print(" mLockingNotified=");
+ pw.println(profile.mLockingNotified);
+ }
+
wcd = mWirelessChargerDetector;
}
@@ -3590,7 +3767,8 @@ public final class PowerManagerService extends SystemService
proto.write(
PowerServiceSettingsAndConfigurationDumpProto
.MAXIMUM_SCREEN_OFF_TIMEOUT_FROM_DEVICE_ADMIN_MS,
- mMaximumScreenOffTimeoutFromDeviceAdmin);
+ // Clamp to int32
+ Math.min(mMaximumScreenOffTimeoutFromDeviceAdmin, Integer.MAX_VALUE));
proto.write(
PowerServiceSettingsAndConfigurationDumpProto
.IS_MAXIMUM_SCREEN_OFF_TIMEOUT_FROM_DEVICE_ADMIN_ENFORCED_LOCKED,
@@ -3686,9 +3864,9 @@ public final class PowerManagerService extends SystemService
mIsVrModeEnabled);
proto.end(settingsAndConfigurationToken);
- final int sleepTimeout = getSleepTimeoutLocked();
- final int screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout);
- final int screenDimDuration = getScreenDimDurationLocked(screenOffTimeout);
+ final long sleepTimeout = getSleepTimeoutLocked();
+ final long screenOffTimeout = getScreenOffTimeoutLocked(sleepTimeout);
+ final long screenDimDuration = getScreenDimDurationLocked(screenOffTimeout);
proto.write(PowerManagerServiceDumpProto.SLEEP_TIMEOUT_MS, sleepTimeout);
proto.write(PowerManagerServiceDumpProto.SCREEN_OFF_TIMEOUT_MS, screenOffTimeout);
proto.write(PowerManagerServiceDumpProto.SCREEN_DIM_DURATION_MS, screenDimDuration);
@@ -4146,7 +4324,7 @@ public final class PowerManagerService extends SystemService
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.DEVICE_POWER, null);
}
- if (ws != null && ws.size() != 0) {
+ if (ws != null && !ws.isEmpty()) {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.UPDATE_DEVICE_STATS, null);
} else {
@@ -4201,7 +4379,7 @@ public final class PowerManagerService extends SystemService
}
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.WAKE_LOCK, null);
- if (ws != null && ws.size() != 0) {
+ if (ws != null && !ws.isEmpty()) {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.UPDATE_DEVICE_STATS, null);
} else {
@@ -4697,8 +4875,8 @@ public final class PowerManagerService extends SystemService
}
@Override
- public void setMaximumScreenOffTimeoutFromDeviceAdmin(int timeMs) {
- setMaximumScreenOffTimeoutFromDeviceAdminInternal(timeMs);
+ public void setMaximumScreenOffTimeoutFromDeviceAdmin(@UserIdInt int userId, long timeMs) {
+ setMaximumScreenOffTimeoutFromDeviceAdminInternal(userId, timeMs);
}
@Override
diff --git a/com/android/server/power/batterysaver/BatterySaverController.java b/com/android/server/power/batterysaver/BatterySaverController.java
index 80bc9359..d4627c2d 100644
--- a/com/android/server/power/batterysaver/BatterySaverController.java
+++ b/com/android/server/power/batterysaver/BatterySaverController.java
@@ -16,12 +16,18 @@
package com.android.server.power.batterysaver;
import android.Manifest;
+import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
+import android.app.Notification;
+import android.app.Notification.BigTextStyle;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.power.V1_0.PowerHint;
+import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -32,11 +38,14 @@ import android.os.PowerSaveState;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
-import android.widget.Toast;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
+import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.power.BatterySaverPolicy;
import com.android.server.power.BatterySaverPolicy.BatterySaverPolicyListener;
@@ -61,12 +70,37 @@ public class BatterySaverController implements BatterySaverPolicyListener {
private final BatterySaverPolicy mBatterySaverPolicy;
+ private static final String WARNING_LINK_URL = "http://goto.google.com/extreme-battery-saver";
+
@GuardedBy("mLock")
private final ArrayList<LowPowerModeListener> mListeners = new ArrayList<>();
@GuardedBy("mLock")
private boolean mEnabled;
+ /**
+ * Previously enabled or not; only for the event logging. Only use it from
+ * {@link #handleBatterySaverStateChanged}.
+ */
+ private boolean mPreviouslyEnabled;
+
+ @GuardedBy("mLock")
+ private boolean mIsInteractive;
+
+ /**
+ * Read-only list of plugins. No need for synchronization.
+ */
+ private final Plugin[] mPlugins;
+
+ /**
+ * Plugin interface. All methods are guaranteed to be called on the same (handler) thread.
+ */
+ public interface Plugin {
+ void onSystemReady(BatterySaverController caller);
+
+ void onBatterySaverChanged(BatterySaverController caller);
+ }
+
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -92,6 +126,12 @@ public class BatterySaverController implements BatterySaverPolicyListener {
mBatterySaverPolicy = policy;
mBatterySaverPolicy.addListener(this);
mFileUpdater = new FileUpdater(context);
+
+ // Initialize plugins.
+ final ArrayList<Plugin> plugins = new ArrayList<>();
+ plugins.add(new BatterySaverLocationPlugin(mContext));
+
+ mPlugins = plugins.toArray(new Plugin[plugins.size()]);
}
/**
@@ -104,7 +144,7 @@ public class BatterySaverController implements BatterySaverPolicyListener {
}
/**
- * Called by {@link PowerManagerService} on system ready..
+ * Called by {@link PowerManagerService} on system ready.
*/
public void systemReady() {
final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
@@ -113,6 +153,7 @@ public class BatterySaverController implements BatterySaverPolicyListener {
mFileUpdater.systemReady(LocalServices.getService(ActivityManagerInternal.class)
.isRuntimeRestarted());
+ mHandler.postSystemReady();
}
private PowerManager getPowerManager() {
@@ -137,6 +178,8 @@ public class BatterySaverController implements BatterySaverPolicyListener {
private static final int ARG_DONT_SEND_BROADCAST = 0;
private static final int ARG_SEND_BROADCAST = 1;
+ private static final int MSG_SYSTEM_READY = 2;
+
public MyHandler(Looper looper) {
super(looper);
}
@@ -146,12 +189,22 @@ public class BatterySaverController implements BatterySaverPolicyListener {
ARG_SEND_BROADCAST : ARG_DONT_SEND_BROADCAST, 0).sendToTarget();
}
+ public void postSystemReady() {
+ obtainMessage(MSG_SYSTEM_READY, 0, 0).sendToTarget();
+ }
+
@Override
public void dispatchMessage(Message msg) {
switch (msg.what) {
case MSG_STATE_CHANGED:
handleBatterySaverStateChanged(msg.arg1 == ARG_SEND_BROADCAST);
break;
+
+ case MSG_SYSTEM_READY:
+ for (Plugin p : mPlugins) {
+ p.onSystemReady(BatterySaverController.this);
+ }
+ break;
}
}
}
@@ -171,12 +224,24 @@ public class BatterySaverController implements BatterySaverPolicyListener {
}
/** @return whether battery saver is enabled or not. */
- boolean isEnabled() {
+ public boolean isEnabled() {
synchronized (mLock) {
return mEnabled;
}
}
+ /** @return whether device is in interactive state. */
+ public boolean isInteractive() {
+ synchronized (mLock) {
+ return mIsInteractive;
+ }
+ }
+
+ /** @return Battery saver policy. */
+ public BatterySaverPolicy getBatterySaverPolicy() {
+ return mBatterySaverPolicy;
+ }
+
/**
* @return true if launch boost should currently be disabled.
*/
@@ -203,11 +268,18 @@ public class BatterySaverController implements BatterySaverPolicyListener {
final ArrayMap<String, String> fileValues;
synchronized (mLock) {
- Slog.i(TAG, "Battery saver " + (mEnabled ? "enabled" : "disabled")
- + ": isInteractive=" + isInteractive);
+ EventLogTags.writeBatterySaverMode(
+ mPreviouslyEnabled ? 1 : 0, // Previously off or on.
+ mEnabled ? 1 : 0, // Now off or on.
+ isInteractive ? 1 : 0, // Device interactive state.
+ mEnabled ? mBatterySaverPolicy.toEventLogString() : "");
+ mPreviouslyEnabled = mEnabled;
listeners = mListeners.toArray(new LowPowerModeListener[mListeners.size()]);
+
enabled = mEnabled;
+ mIsInteractive = isInteractive;
+
if (enabled) {
fileValues = mBatterySaverPolicy.getFileValues(isInteractive);
@@ -227,12 +299,16 @@ public class BatterySaverController implements BatterySaverPolicyListener {
mFileUpdater.writeFiles(fileValues);
}
+ for (Plugin p : mPlugins) {
+ p.onBatterySaverChanged(this);
+ }
+
if (sendBroadcast) {
if (enabled) {
// STOPSHIP Remove the toast.
- Toast.makeText(mContext,
- com.android.internal.R.string.battery_saver_warning,
- Toast.LENGTH_LONG).show();
+ postWarningNotification();
+ } else {
+ cancelWarningNotification();
}
if (DEBUG) {
@@ -265,4 +341,51 @@ public class BatterySaverController implements BatterySaverPolicyListener {
}
}
}
+
+ private void postWarningNotification() {
+ final UserHandle foregroundUser = UserHandle.of(ActivityManager.getCurrentUser());
+
+ final PendingIntent pendingIntent = PendingIntent
+ .getActivityAsUser(mContext, 0,
+ new Intent(Intent.ACTION_VIEW, Uri.parse(WARNING_LINK_URL)),
+ PendingIntent.FLAG_CANCEL_CURRENT, null,
+ foregroundUser);
+
+ final CharSequence title = mContext.getString
+ (com.android.internal.R.string.battery_saver_warning_title);
+ final CharSequence text = mContext.getString
+ (com.android.internal.R.string.battery_saver_warning);
+
+ final Notification notification =
+ new Notification.Builder(mContext, SystemNotificationChannels.ALERTS)
+ .setSmallIcon(R.drawable.stat_notify_error)
+ .setTicker(title)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(pendingIntent)
+ .setStyle(new BigTextStyle().bigText(text))
+ .build();
+
+ final NotificationManager nm = mContext.getSystemService(NotificationManager.class);
+
+ if (nm != null) {
+ nm.notifyAsUser(title.toString(),
+ SystemMessage.NOTE_BATTERY_SAVER_WARNING,
+ notification,
+ foregroundUser);
+ }
+ }
+
+ private void cancelWarningNotification() {
+ final UserHandle foregroundUser = UserHandle.of(ActivityManager.getCurrentUser());
+ final CharSequence title = mContext.getString
+ (com.android.internal.R.string.battery_saver_warning_title);
+
+ final NotificationManager nm = mContext.getSystemService(NotificationManager.class);
+ if (nm != null) {
+ nm.cancelAsUser(title.toString(), SystemMessage.NOTE_BATTERY_SAVER_WARNING,
+ foregroundUser);
+ }
+ }
}
diff --git a/com/android/server/power/batterysaver/BatterySaverLocationPlugin.java b/com/android/server/power/batterysaver/BatterySaverLocationPlugin.java
new file mode 100644
index 00000000..0af19b6f
--- /dev/null
+++ b/com/android/server/power/batterysaver/BatterySaverLocationPlugin.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.android.server.power.batterysaver;
+
+import android.content.Context;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.util.Slog;
+
+import com.android.server.power.BatterySaverPolicy;
+import com.android.server.power.batterysaver.BatterySaverController.Plugin;
+
+public class BatterySaverLocationPlugin implements Plugin {
+ private static final String TAG = "BatterySaverLocationPlugin";
+
+ private static final boolean DEBUG = BatterySaverController.DEBUG;
+
+ private final Context mContext;
+
+ public BatterySaverLocationPlugin(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void onBatterySaverChanged(BatterySaverController caller) {
+ if (DEBUG) {
+ Slog.d(TAG, "onBatterySaverChanged");
+ }
+ updateLocationState(caller);
+ }
+
+ @Override
+ public void onSystemReady(BatterySaverController caller) {
+ if (DEBUG) {
+ Slog.d(TAG, "onSystemReady");
+ }
+ updateLocationState(caller);
+ }
+
+ private void updateLocationState(BatterySaverController caller) {
+ final boolean kill =
+ (caller.getBatterySaverPolicy().getGpsMode()
+ == BatterySaverPolicy.GPS_MODE_ALL_DISABLED_WHEN_SCREEN_OFF) &&
+ caller.isEnabled() && !caller.isInteractive();
+
+ if (DEBUG) {
+ Slog.d(TAG, "Battery saver " + (kill ? "stopping" : "restoring") + " location.");
+ }
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Global.LOCATION_GLOBAL_KILL_SWITCH, kill ? 1 : 0);
+ }
+}
diff --git a/com/android/server/power/batterysaver/CpuFrequencies.java b/com/android/server/power/batterysaver/CpuFrequencies.java
index 1629486b..f2485098 100644
--- a/com/android/server/power/batterysaver/CpuFrequencies.java
+++ b/com/android/server/power/batterysaver/CpuFrequencies.java
@@ -54,6 +54,10 @@ public class CpuFrequencies {
mCoreAndFrequencies.clear();
try {
for (String pair : cpuNumberAndFrequencies.split("/")) {
+ pair = pair.trim();
+ if (pair.length() == 0) {
+ continue;
+ }
final String[] coreAndFreq = pair.split(":", 2);
if (coreAndFreq.length != 2) {
@@ -65,7 +69,7 @@ public class CpuFrequencies {
mCoreAndFrequencies.put(core, freq);
}
} catch (IllegalArgumentException e) {
- Slog.wtf(TAG, "Invalid configuration: " + cpuNumberAndFrequencies, e);
+ Slog.wtf(TAG, "Invalid configuration: '" + cpuNumberAndFrequencies + "'");
}
}
return this;
diff --git a/com/android/server/print/UserState.java b/com/android/server/print/UserState.java
index e8ae020c..364bbc03 100644
--- a/com/android/server/print/UserState.java
+++ b/com/android/server/print/UserState.java
@@ -25,6 +25,7 @@ import static com.android.internal.print.DumpUtils.writePrintJobInfo;
import static com.android.internal.print.DumpUtils.writePrinterId;
import static com.android.internal.print.DumpUtils.writePrinterInfo;
import static com.android.internal.print.DumpUtils.writeStringIfNotNull;
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -81,7 +82,6 @@ import com.android.internal.R;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.os.BackgroundThread;
-import com.android.internal.os.SomeArgs;
import com.android.server.print.RemotePrintService.PrintServiceCallbacks;
import com.android.server.print.RemotePrintServiceRecommendationService
.RemotePrintServiceRecommendationServiceCallbacks;
@@ -462,7 +462,7 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
if (mPrinterDiscoverySession == null) {
// If we do not have a session, tell all service to create one.
- mPrinterDiscoverySession = new PrinterDiscoverySessionMediator(mContext) {
+ mPrinterDiscoverySession = new PrinterDiscoverySessionMediator() {
@Override
public void onDestroyed() {
mPrinterDiscoverySession = null;
@@ -1141,12 +1141,8 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
// just died. Do this off the main thread since we do to allow
// calls into the spooler on the main thread.
if (Looper.getMainLooper().isCurrentThread()) {
- BackgroundThread.getHandler().post(new Runnable() {
- @Override
- public void run() {
- failScheduledPrintJobsForServiceInternal(serviceName);
- }
- });
+ BackgroundThread.getHandler().sendMessage(obtainMessage(
+ UserState::failScheduledPrintJobsForServiceInternal, this, serviceName));
} else {
failScheduledPrintJobsForServiceInternal(serviceName);
}
@@ -1341,18 +1337,13 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
private final List<PrinterId> mStateTrackedPrinters = new ArrayList<PrinterId>();
- private final Handler mSessionHandler;
-
private boolean mIsDestroyed;
- public PrinterDiscoverySessionMediator(Context context) {
- mSessionHandler = new SessionHandler(context.getMainLooper());
+ PrinterDiscoverySessionMediator() {
// Kick off the session creation.
- List<RemotePrintService> services = new ArrayList<RemotePrintService>(
- mActiveServices.values());
- mSessionHandler.obtainMessage(SessionHandler
- .MSG_DISPATCH_CREATE_PRINTER_DISCOVERY_SESSION, services)
- .sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
+ handleDispatchCreatePrinterDiscoverySession,
+ this, new ArrayList<>(mActiveServices.values())));
}
public void addObserverLocked(@NonNull IPrinterDiscoveryObserver observer) {
@@ -1361,12 +1352,9 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
// Bring the added observer up to speed with the printers.
if (!mPrinters.isEmpty()) {
- List<PrinterInfo> printers = new ArrayList<PrinterInfo>(mPrinters.values());
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = observer;
- args.arg2 = printers;
- mSessionHandler.obtainMessage(SessionHandler.MSG_PRINTERS_ADDED,
- args).sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(
+ UserState.PrinterDiscoverySessionMediator::handlePrintersAdded,
+ this, observer, new ArrayList<>(mPrinters.values())));
}
}
@@ -1403,14 +1391,9 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
return;
}
- List<RemotePrintService> services = new ArrayList<RemotePrintService>(
- mActiveServices.values());
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = services;
- args.arg2 = priorityList;
- mSessionHandler.obtainMessage(SessionHandler
- .MSG_DISPATCH_START_PRINTER_DISCOVERY, args)
- .sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
+ handleDispatchStartPrinterDiscovery, this,
+ new ArrayList<>(mActiveServices.values()), priorityList));
}
public final void stopPrinterDiscoveryLocked(@NonNull IPrinterDiscoveryObserver observer) {
@@ -1426,11 +1409,9 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
if (!mStartedPrinterDiscoveryTokens.isEmpty()) {
return;
}
- List<RemotePrintService> services = new ArrayList<RemotePrintService>(
- mActiveServices.values());
- mSessionHandler.obtainMessage(SessionHandler
- .MSG_DISPATCH_STOP_PRINTER_DISCOVERY, services)
- .sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
+ handleDispatchStopPrinterDiscovery,
+ this, new ArrayList<>(mActiveServices.values())));
}
public void validatePrintersLocked(@NonNull List<PrinterId> printerIds) {
@@ -1461,12 +1442,9 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
// Schedule a notification of the service.
RemotePrintService service = mActiveServices.get(serviceName);
if (service != null) {
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = service;
- args.arg2 = updateList;
- mSessionHandler.obtainMessage(SessionHandler
- .MSG_VALIDATE_PRINTERS, args)
- .sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(
+ UserState.PrinterDiscoverySessionMediator::handleValidatePrinters,
+ this, service, updateList));
}
}
}
@@ -1493,12 +1471,8 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
return;
}
// Ask the service to start tracking.
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = service;
- args.arg2 = printerId;
- mSessionHandler.obtainMessage(SessionHandler
- .MSG_START_PRINTER_STATE_TRACKING, args)
- .sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
+ handleStartPrinterStateTracking, this, service, printerId));
}
public final void stopPrinterStateTrackingLocked(PrinterId printerId) {
@@ -1520,12 +1494,8 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
return;
}
// Ask the service to start tracking.
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = service;
- args.arg2 = printerId;
- mSessionHandler.obtainMessage(SessionHandler
- .MSG_STOP_PRINTER_STATE_TRACKING, args)
- .sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
+ handleStopPrinterStateTracking, this, service, printerId));
}
public void onDestroyed() {
@@ -1551,11 +1521,9 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
stopPrinterDiscoveryLocked(IPrinterDiscoveryObserver.Stub.asInterface(token));
}
// Tell the services we are done.
- List<RemotePrintService> services = new ArrayList<RemotePrintService>(
- mActiveServices.values());
- mSessionHandler.obtainMessage(SessionHandler
- .MSG_DISPATCH_DESTROY_PRINTER_DISCOVERY_SESSION, services)
- .sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
+ handleDispatchDestroyPrinterDiscoverySession,
+ this, new ArrayList<>(mActiveServices.values())));
}
public void onPrintersAddedLocked(List<PrinterInfo> printers) {
@@ -1579,8 +1547,9 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
}
}
if (addedPrinters != null) {
- mSessionHandler.obtainMessage(SessionHandler.MSG_DISPATCH_PRINTERS_ADDED,
- addedPrinters).sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(
+ UserState.PrinterDiscoverySessionMediator::handleDispatchPrintersAdded,
+ this, addedPrinters));
}
}
@@ -1604,8 +1573,9 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
}
}
if (removedPrinterIds != null) {
- mSessionHandler.obtainMessage(SessionHandler.MSG_DISPATCH_PRINTERS_REMOVED,
- removedPrinterIds).sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(
+ UserState.PrinterDiscoverySessionMediator::handleDispatchPrintersRemoved,
+ this, removedPrinterIds));
}
}
@@ -1646,8 +1616,9 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
ArrayList<PrinterInfo> addedPrinters = new ArrayList<>(1);
addedPrinters.add(newPrinter);
- mSessionHandler.obtainMessage(SessionHandler.MSG_DISPATCH_PRINTERS_ADDED,
- addedPrinters).sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(
+ UserState.PrinterDiscoverySessionMediator::handleDispatchPrintersAdded,
+ this, addedPrinters));
}
}
@@ -1661,26 +1632,20 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
return;
}
// Tell the service to create a session.
- mSessionHandler.obtainMessage(
- SessionHandler.MSG_CREATE_PRINTER_DISCOVERY_SESSION,
- service).sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(
+ RemotePrintService::createPrinterDiscoverySession, service));
// Start printer discovery if necessary.
if (!mStartedPrinterDiscoveryTokens.isEmpty()) {
- mSessionHandler.obtainMessage(
- SessionHandler.MSG_START_PRINTER_DISCOVERY,
- service).sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(
+ RemotePrintService::startPrinterDiscovery, service, null));
}
// Start tracking printers if necessary
final int trackedPrinterCount = mStateTrackedPrinters.size();
for (int i = 0; i < trackedPrinterCount; i++) {
PrinterId printerId = mStateTrackedPrinters.get(i);
if (printerId.getServiceName().equals(service.getComponentName())) {
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = service;
- args.arg2 = printerId;
- mSessionHandler.obtainMessage(SessionHandler
- .MSG_START_PRINTER_STATE_TRACKING, args)
- .sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(
+ RemotePrintService::startPrinterStateTracking, service, printerId));
}
}
}
@@ -1781,9 +1746,9 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
for (int i = 0; i < removedPrinterCount; i++) {
mPrinters.remove(removedPrinterIds.get(i));
}
- mSessionHandler.obtainMessage(
- SessionHandler.MSG_DISPATCH_PRINTERS_REMOVED,
- removedPrinterIds).sendToTarget();
+ Handler.getMain().sendMessage(obtainMessage(
+ UserState.PrinterDiscoverySessionMediator::handleDispatchPrintersRemoved,
+ this, removedPrinterIds));
}
}
@@ -1873,134 +1838,6 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
Log.e(LOG_TAG, "Error sending removed printers", re);
}
}
-
- private final class SessionHandler extends Handler {
- public static final int MSG_PRINTERS_ADDED = 1;
- public static final int MSG_PRINTERS_REMOVED = 2;
- public static final int MSG_DISPATCH_PRINTERS_ADDED = 3;
- public static final int MSG_DISPATCH_PRINTERS_REMOVED = 4;
-
- public static final int MSG_CREATE_PRINTER_DISCOVERY_SESSION = 5;
- public static final int MSG_DESTROY_PRINTER_DISCOVERY_SESSION = 6;
- public static final int MSG_START_PRINTER_DISCOVERY = 7;
- public static final int MSG_STOP_PRINTER_DISCOVERY = 8;
- public static final int MSG_DISPATCH_CREATE_PRINTER_DISCOVERY_SESSION = 9;
- public static final int MSG_DISPATCH_DESTROY_PRINTER_DISCOVERY_SESSION = 10;
- public static final int MSG_DISPATCH_START_PRINTER_DISCOVERY = 11;
- public static final int MSG_DISPATCH_STOP_PRINTER_DISCOVERY = 12;
- public static final int MSG_VALIDATE_PRINTERS = 13;
- public static final int MSG_START_PRINTER_STATE_TRACKING = 14;
- public static final int MSG_STOP_PRINTER_STATE_TRACKING = 15;
- public static final int MSG_DESTROY_SERVICE = 16;
-
- SessionHandler(Looper looper) {
- super(looper, null, false);
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public void handleMessage(Message message) {
- switch (message.what) {
- case MSG_PRINTERS_ADDED: {
- SomeArgs args = (SomeArgs) message.obj;
- IPrinterDiscoveryObserver observer = (IPrinterDiscoveryObserver) args.arg1;
- List<PrinterInfo> addedPrinters = (List<PrinterInfo>) args.arg2;
- args.recycle();
- handlePrintersAdded(observer, addedPrinters);
- } break;
-
- case MSG_PRINTERS_REMOVED: {
- SomeArgs args = (SomeArgs) message.obj;
- IPrinterDiscoveryObserver observer = (IPrinterDiscoveryObserver) args.arg1;
- List<PrinterId> removedPrinterIds = (List<PrinterId>) args.arg2;
- args.recycle();
- handlePrintersRemoved(observer, removedPrinterIds);
- }
-
- case MSG_DISPATCH_PRINTERS_ADDED: {
- List<PrinterInfo> addedPrinters = (List<PrinterInfo>) message.obj;
- handleDispatchPrintersAdded(addedPrinters);
- } break;
-
- case MSG_DISPATCH_PRINTERS_REMOVED: {
- List<PrinterId> removedPrinterIds = (List<PrinterId>) message.obj;
- handleDispatchPrintersRemoved(removedPrinterIds);
- } break;
-
- case MSG_CREATE_PRINTER_DISCOVERY_SESSION: {
- RemotePrintService service = (RemotePrintService) message.obj;
- service.createPrinterDiscoverySession();
- } break;
-
- case MSG_DESTROY_PRINTER_DISCOVERY_SESSION: {
- RemotePrintService service = (RemotePrintService) message.obj;
- service.destroyPrinterDiscoverySession();
- } break;
-
- case MSG_START_PRINTER_DISCOVERY: {
- RemotePrintService service = (RemotePrintService) message.obj;
- service.startPrinterDiscovery(null);
- } break;
-
- case MSG_STOP_PRINTER_DISCOVERY: {
- RemotePrintService service = (RemotePrintService) message.obj;
- service.stopPrinterDiscovery();
- } break;
-
- case MSG_DISPATCH_CREATE_PRINTER_DISCOVERY_SESSION: {
- List<RemotePrintService> services = (List<RemotePrintService>) message.obj;
- handleDispatchCreatePrinterDiscoverySession(services);
- } break;
-
- case MSG_DISPATCH_DESTROY_PRINTER_DISCOVERY_SESSION: {
- List<RemotePrintService> services = (List<RemotePrintService>) message.obj;
- handleDispatchDestroyPrinterDiscoverySession(services);
- } break;
-
- case MSG_DISPATCH_START_PRINTER_DISCOVERY: {
- SomeArgs args = (SomeArgs) message.obj;
- List<RemotePrintService> services = (List<RemotePrintService>) args.arg1;
- List<PrinterId> printerIds = (List<PrinterId>) args.arg2;
- args.recycle();
- handleDispatchStartPrinterDiscovery(services, printerIds);
- } break;
-
- case MSG_DISPATCH_STOP_PRINTER_DISCOVERY: {
- List<RemotePrintService> services = (List<RemotePrintService>) message.obj;
- handleDispatchStopPrinterDiscovery(services);
- } break;
-
- case MSG_VALIDATE_PRINTERS: {
- SomeArgs args = (SomeArgs) message.obj;
- RemotePrintService service = (RemotePrintService) args.arg1;
- List<PrinterId> printerIds = (List<PrinterId>) args.arg2;
- args.recycle();
- handleValidatePrinters(service, printerIds);
- } break;
-
- case MSG_START_PRINTER_STATE_TRACKING: {
- SomeArgs args = (SomeArgs) message.obj;
- RemotePrintService service = (RemotePrintService) args.arg1;
- PrinterId printerId = (PrinterId) args.arg2;
- args.recycle();
- handleStartPrinterStateTracking(service, printerId);
- } break;
-
- case MSG_STOP_PRINTER_STATE_TRACKING: {
- SomeArgs args = (SomeArgs) message.obj;
- RemotePrintService service = (RemotePrintService) args.arg1;
- PrinterId printerId = (PrinterId) args.arg2;
- args.recycle();
- handleStopPrinterStateTracking(service, printerId);
- } break;
-
- case MSG_DESTROY_SERVICE: {
- RemotePrintService service = (RemotePrintService) message.obj;
- service.destroy();
- } break;
- }
- }
- }
}
private final class PrintJobForAppCache {
diff --git a/com/android/server/security/KeyAttestationApplicationIdProviderService.java b/com/android/server/security/KeyAttestationApplicationIdProviderService.java
index ab9ab671..a8c68c07 100644
--- a/com/android/server/security/KeyAttestationApplicationIdProviderService.java
+++ b/com/android/server/security/KeyAttestationApplicationIdProviderService.java
@@ -63,7 +63,7 @@ public class KeyAttestationApplicationIdProviderService
PackageInfo packageInfo = mPackageManager.getPackageInfoAsUser(packageNames[i],
PackageManager.GET_SIGNATURES, userId);
keyAttestationPackageInfos[i] = new KeyAttestationPackageInfo(packageNames[i],
- packageInfo.versionCode, packageInfo.signatures);
+ packageInfo.getLongVersionCode(), packageInfo.signatures);
}
} catch (NameNotFoundException nnfe) {
throw new RemoteException(nnfe.getMessage());
diff --git a/com/android/server/slice/PinnedSliceState.java b/com/android/server/slice/PinnedSliceState.java
new file mode 100644
index 00000000..cf930f5c
--- /dev/null
+++ b/com/android/server/slice/PinnedSliceState.java
@@ -0,0 +1,215 @@
+/*
+ * 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 com.android.server.slice;
+
+import android.app.slice.ISliceListener;
+import android.app.slice.Slice;
+import android.app.slice.SliceProvider;
+import android.app.slice.SliceSpec;
+import android.content.ContentProviderClient;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Manages the state of a pinned slice.
+ */
+public class PinnedSliceState {
+
+ private static final long SLICE_TIMEOUT = 5000;
+ private static final String TAG = "PinnedSliceState";
+
+ private final Object mLock;
+
+ private final SliceManagerService mService;
+ private final Uri mUri;
+ @GuardedBy("mLock")
+ private final ArraySet<String> mPinnedPkgs = new ArraySet<>();
+ @GuardedBy("mLock")
+ private final ArraySet<ISliceListener> mListeners = new ArraySet<>();
+ @GuardedBy("mLock")
+ private SliceSpec[] mSupportedSpecs = null;
+
+ public PinnedSliceState(SliceManagerService service, Uri uri) {
+ mService = service;
+ mUri = uri;
+ mService.getHandler().post(this::handleSendPinned);
+ mLock = mService.getLock();
+ }
+
+ public SliceSpec[] getSpecs() {
+ return mSupportedSpecs;
+ }
+
+ public void mergeSpecs(SliceSpec[] supportedSpecs) {
+ synchronized (mLock) {
+ if (mSupportedSpecs == null) {
+ mSupportedSpecs = supportedSpecs;
+ } else {
+ List<SliceSpec> specs = Arrays.asList(mSupportedSpecs);
+ mSupportedSpecs = specs.stream().map(s -> {
+ SliceSpec other = findSpec(supportedSpecs, s.getType());
+ if (other == null) return null;
+ if (other.getRevision() < s.getRevision()) {
+ return other;
+ }
+ return s;
+ }).filter(s -> s != null).toArray(SliceSpec[]::new);
+ }
+ }
+ }
+
+ private SliceSpec findSpec(SliceSpec[] specs, String type) {
+ for (SliceSpec spec : specs) {
+ if (Objects.equals(spec.getType(), type)) {
+ return spec;
+ }
+ }
+ return null;
+ }
+
+ public Uri getUri() {
+ return mUri;
+ }
+
+ public void destroy() {
+ mService.getHandler().post(this::handleSendUnpinned);
+ }
+
+ public void onChange() {
+ mService.getHandler().post(this::handleBind);
+ }
+
+ public void addSliceListener(ISliceListener listener, SliceSpec[] specs) {
+ synchronized (mLock) {
+ if (mListeners.add(listener) && mListeners.size() == 1) {
+ mService.listen(mUri);
+ }
+ mergeSpecs(specs);
+ }
+ }
+
+ public boolean removeSliceListener(ISliceListener listener) {
+ synchronized (mLock) {
+ if (mListeners.remove(listener) && mListeners.size() == 0) {
+ mService.unlisten(mUri);
+ }
+ }
+ return !isPinned();
+ }
+
+ public void pin(String pkg, SliceSpec[] specs) {
+ synchronized (mLock) {
+ mPinnedPkgs.add(pkg);
+ mergeSpecs(specs);
+ }
+ }
+
+ public boolean unpin(String pkg) {
+ synchronized (mLock) {
+ mPinnedPkgs.remove(pkg);
+ }
+ return !isPinned();
+ }
+
+ public boolean isListening() {
+ synchronized (mLock) {
+ return !mListeners.isEmpty();
+ }
+ }
+
+ @VisibleForTesting
+ public boolean isPinned() {
+ synchronized (mLock) {
+ return !mPinnedPkgs.isEmpty() || !mListeners.isEmpty();
+ }
+ }
+
+ ContentProviderClient getClient() {
+ ContentProviderClient client =
+ mService.getContext().getContentResolver().acquireContentProviderClient(mUri);
+ client.setDetectNotResponding(SLICE_TIMEOUT);
+ return client;
+ }
+
+ private void handleBind() {
+ Slice s;
+ try (ContentProviderClient client = getClient()) {
+ Bundle extras = new Bundle();
+ extras.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri);
+ extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
+ new ArrayList<>(Arrays.asList(mSupportedSpecs)));
+ final Bundle res;
+ try {
+ res = client.call(SliceProvider.METHOD_SLICE, null, extras);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to bind slice " + mUri, e);
+ return;
+ }
+ if (res == null) return;
+ Bundle.setDefusable(res, true);
+ s = res.getParcelable(SliceProvider.EXTRA_SLICE);
+ }
+ synchronized (mLock) {
+ mListeners.removeIf(l -> {
+ try {
+ l.onSliceUpdated(s);
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to notify slice " + mUri, e);
+ return true;
+ }
+ });
+ if (!isPinned()) {
+ // All the listeners died, remove from pinned state.
+ mService.removePinnedSlice(mUri);
+ }
+ }
+ }
+
+ private void handleSendPinned() {
+ try (ContentProviderClient client = getClient()) {
+ Bundle b = new Bundle();
+ b.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri);
+ try {
+ client.call(SliceProvider.METHOD_PIN, null, b);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to contact " + mUri, e);
+ }
+ }
+ }
+
+ private void handleSendUnpinned() {
+ try (ContentProviderClient client = getClient()) {
+ Bundle b = new Bundle();
+ b.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri);
+ try {
+ client.call(SliceProvider.METHOD_UNPIN, null, b);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to contact " + mUri, e);
+ }
+ }
+ }
+}
diff --git a/com/android/server/slice/SliceManagerService.java b/com/android/server/slice/SliceManagerService.java
new file mode 100644
index 00000000..2d9e772a
--- /dev/null
+++ b/com/android/server/slice/SliceManagerService.java
@@ -0,0 +1,352 @@
+/*
+ * 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 com.android.server.slice;
+
+import static android.content.ContentProvider.getUserIdFromUri;
+import static android.content.ContentProvider.maybeAddUserId;
+
+import android.Manifest.permission;
+import android.app.AppOpsManager;
+import android.app.slice.ISliceListener;
+import android.app.slice.ISliceManager;
+import android.app.slice.SliceSpec;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.AssistUtils;
+import com.android.internal.util.Preconditions;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
+import com.android.server.SystemService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class SliceManagerService extends ISliceManager.Stub {
+
+ private static final String TAG = "SliceManagerService";
+ private final Object mLock = new Object();
+
+ private final Context mContext;
+ private final PackageManagerInternal mPackageManagerInternal;
+ private final AppOpsManager mAppOps;
+ private final AssistUtils mAssistUtils;
+
+ @GuardedBy("mLock")
+ private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>();
+ private final Handler mHandler;
+ private final ContentObserver mObserver;
+
+ public SliceManagerService(Context context) {
+ this(context, createHandler().getLooper());
+ }
+
+ @VisibleForTesting
+ SliceManagerService(Context context, Looper looper) {
+ mContext = context;
+ mPackageManagerInternal = Preconditions.checkNotNull(
+ LocalServices.getService(PackageManagerInternal.class));
+ mAppOps = context.getSystemService(AppOpsManager.class);
+ mAssistUtils = new AssistUtils(context);
+ mHandler = new Handler(looper);
+
+ mObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ try {
+ getPinnedSlice(uri).onChange();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Received change for unpinned slice " + uri, e);
+ }
+ }
+ };
+ }
+
+ /// ----- Lifecycle stuff -----
+ private void systemReady() {
+ }
+
+ private void onUnlockUser(int userId) {
+ }
+
+ private void onStopUser(int userId) {
+ synchronized (mLock) {
+ mPinnedSlicesByUri.values().removeIf(s -> getUserIdFromUri(s.getUri()) == userId);
+ }
+ }
+
+ /// ----- ISliceManager stuff -----
+ @Override
+ public void addSliceListener(Uri uri, String pkg, ISliceListener listener, SliceSpec[] specs)
+ throws RemoteException {
+ verifyCaller(pkg);
+ uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
+ enforceAccess(pkg, uri);
+ getOrCreatePinnedSlice(uri).addSliceListener(listener, specs);
+ }
+
+ @Override
+ public void removeSliceListener(Uri uri, String pkg, ISliceListener listener)
+ throws RemoteException {
+ verifyCaller(pkg);
+ uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
+ enforceAccess(pkg, uri);
+ if (getPinnedSlice(uri).removeSliceListener(listener)) {
+ removePinnedSlice(uri);
+ }
+ }
+
+ @Override
+ public void pinSlice(String pkg, Uri uri, SliceSpec[] specs) throws RemoteException {
+ verifyCaller(pkg);
+ enforceFullAccess(pkg, "pinSlice", uri);
+ uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
+ getOrCreatePinnedSlice(uri).pin(pkg, specs);
+ }
+
+ @Override
+ public void unpinSlice(String pkg, Uri uri) throws RemoteException {
+ verifyCaller(pkg);
+ enforceFullAccess(pkg, "unpinSlice", uri);
+ uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
+ if (getPinnedSlice(uri).unpin(pkg)) {
+ removePinnedSlice(uri);
+ }
+ }
+
+ @Override
+ public boolean hasSliceAccess(String pkg) throws RemoteException {
+ verifyCaller(pkg);
+ return hasFullSliceAccess(pkg, Binder.getCallingUserHandle().getIdentifier());
+ }
+
+ @Override
+ public SliceSpec[] getPinnedSpecs(Uri uri, String pkg) throws RemoteException {
+ verifyCaller(pkg);
+ enforceAccess(pkg, uri);
+ return getPinnedSlice(uri).getSpecs();
+ }
+
+ /// ----- internal code -----
+ void removePinnedSlice(Uri uri) {
+ synchronized (mLock) {
+ mPinnedSlicesByUri.remove(uri).destroy();
+ }
+ }
+
+ private PinnedSliceState getPinnedSlice(Uri uri) {
+ synchronized (mLock) {
+ PinnedSliceState manager = mPinnedSlicesByUri.get(uri);
+ if (manager == null) {
+ throw new IllegalStateException(String.format("Slice %s not pinned",
+ uri.toString()));
+ }
+ return manager;
+ }
+ }
+
+ private PinnedSliceState getOrCreatePinnedSlice(Uri uri) {
+ synchronized (mLock) {
+ PinnedSliceState manager = mPinnedSlicesByUri.get(uri);
+ if (manager == null) {
+ manager = createPinnedSlice(uri);
+ mPinnedSlicesByUri.put(uri, manager);
+ }
+ return manager;
+ }
+ }
+
+ @VisibleForTesting
+ PinnedSliceState createPinnedSlice(Uri uri) {
+ return new PinnedSliceState(this, uri);
+ }
+
+ public Object getLock() {
+ return mLock;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ private void enforceAccess(String pkg, Uri uri) {
+ 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");
+ int user = Binder.getCallingUserHandle().getIdentifier();
+ if (getUserIdFromUri(uri, user) != user) {
+ getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL,
+ "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL");
+ }
+ }
+
+ private void enforceFullAccess(String pkg, String name, Uri uri) {
+ int user = Binder.getCallingUserHandle().getIdentifier();
+ if (!hasFullSliceAccess(pkg, user)) {
+ throw new SecurityException(String.format("Call %s requires full slice access", name));
+ }
+ if (getUserIdFromUri(uri, user) != user) {
+ getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL,
+ "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL");
+ }
+ }
+
+ private void verifyCaller(String pkg) {
+ mAppOps.checkPackage(Binder.getCallingUid(), pkg);
+ }
+
+ private boolean hasFullSliceAccess(String pkg, int userId) {
+ return isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId)
+ || isGrantedFullAccess(pkg, userId);
+ }
+
+ private boolean isAssistant(String pkg, int userId) {
+ final ComponentName cn = mAssistUtils.getAssistComponentForUser(userId);
+ if (cn == null) {
+ return false;
+ }
+ return cn.getPackageName().equals(pkg);
+ }
+
+ public void listen(Uri uri) {
+ mContext.getContentResolver().registerContentObserver(uri, true, mObserver);
+ }
+
+ public void unlisten(Uri uri) {
+ mContext.getContentResolver().unregisterContentObserver(mObserver);
+ synchronized (mLock) {
+ mPinnedSlicesByUri.forEach((u, s) -> {
+ if (s.isListening()) {
+ listen(u);
+ }
+ });
+ }
+ }
+
+ private boolean isDefaultHomeApp(String pkg, int userId) {
+ String defaultHome = getDefaultHome(userId);
+ return Objects.equals(pkg, defaultHome);
+ }
+
+ // Based on getDefaultHome in ShortcutService.
+ // TODO: Unify if possible
+ @VisibleForTesting
+ String getDefaultHome(int userId) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final List<ResolveInfo> allHomeCandidates = new ArrayList<>();
+
+ // Default launcher from package manager.
+ final ComponentName defaultLauncher = mPackageManagerInternal
+ .getHomeActivitiesAsUser(allHomeCandidates, userId);
+
+ ComponentName detected = null;
+ if (defaultLauncher != null) {
+ detected = defaultLauncher;
+ }
+
+ if (detected == null) {
+ // If we reach here, that means it's the first check since the user was created,
+ // and there's already multiple launchers and there's no default set.
+ // Find the system one with the highest priority.
+ // (We need to check the priority too because of FallbackHome in Settings.)
+ // If there's no system launcher yet, then no one can access slices, until
+ // the user explicitly sets one.
+ final int size = allHomeCandidates.size();
+
+ int lastPriority = Integer.MIN_VALUE;
+ for (int i = 0; i < size; i++) {
+ final ResolveInfo ri = allHomeCandidates.get(i);
+ if (!ri.activityInfo.applicationInfo.isSystemApp()) {
+ continue;
+ }
+ if (ri.priority < lastPriority) {
+ continue;
+ }
+ detected = ri.activityInfo.getComponentName();
+ lastPriority = ri.priority;
+ }
+ }
+ return detected.getPackageName();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private boolean isGrantedFullAccess(String pkg, int userId) {
+ // TODO: This will be user granted access, if we allow this through a prompt.
+ return false;
+ }
+
+ private static ServiceThread createHandler() {
+ ServiceThread handlerThread = new ServiceThread(TAG,
+ Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
+ handlerThread.start();
+ return handlerThread;
+ }
+
+ public static class Lifecycle extends SystemService {
+ private SliceManagerService mService;
+
+ public Lifecycle(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ mService = new SliceManagerService(getContext());
+ publishBinderService(Context.SLICE_SERVICE, mService);
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+ mService.systemReady();
+ }
+ }
+
+ @Override
+ public void onUnlockUser(int userHandle) {
+ mService.onUnlockUser(userHandle);
+ }
+
+ @Override
+ public void onStopUser(int userHandle) {
+ mService.onStopUser(userHandle);
+ }
+ }
+}
diff --git a/com/android/server/soundtrigger/SoundTriggerService.java b/com/android/server/soundtrigger/SoundTriggerService.java
index 51c805da..cd3fdeef 100644
--- a/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/com/android/server/soundtrigger/SoundTriggerService.java
@@ -66,6 +66,7 @@ public class SoundTriggerService extends SystemService {
private SoundTriggerDbHelper mDbHelper;
private SoundTriggerHelper mSoundTriggerHelper;
private final TreeMap<UUID, SoundModel> mLoadedModels;
+ private Object mCallbacksLock;
private final TreeMap<UUID, LocalSoundTriggerRecognitionStatusCallback> mIntentCallbacks;
private PowerManager.WakeLock mWakelock;
@@ -75,6 +76,7 @@ public class SoundTriggerService extends SystemService {
mServiceStub = new SoundTriggerServiceStub();
mLocalSoundTriggerService = new LocalSoundTriggerService(context);
mLoadedModels = new TreeMap<UUID, SoundModel>();
+ mCallbacksLock = new Object();
mIntentCallbacks = new TreeMap<UUID, LocalSoundTriggerRecognitionStatusCallback>();
mLock = new Object();
}
@@ -211,7 +213,9 @@ public class SoundTriggerService extends SystemService {
// don't know if the other model is loaded.
if (oldModel != null && !oldModel.equals(soundModel)) {
mSoundTriggerHelper.unloadGenericSoundModel(soundModel.uuid);
- mIntentCallbacks.remove(soundModel.uuid);
+ synchronized (mCallbacksLock) {
+ mIntentCallbacks.remove(soundModel.uuid);
+ }
}
mLoadedModels.put(soundModel.uuid, soundModel);
}
@@ -240,7 +244,9 @@ public class SoundTriggerService extends SystemService {
// don't know if the other model is loaded.
if (oldModel != null && !oldModel.equals(soundModel)) {
mSoundTriggerHelper.unloadKeyphraseSoundModel(soundModel.keyphrases[0].id);
- mIntentCallbacks.remove(soundModel.uuid);
+ synchronized (mCallbacksLock) {
+ mIntentCallbacks.remove(soundModel.uuid);
+ }
}
mLoadedModels.put(soundModel.uuid, soundModel);
}
@@ -262,8 +268,10 @@ public class SoundTriggerService extends SystemService {
Slog.e(TAG, soundModelId + " is not loaded");
return STATUS_ERROR;
}
- LocalSoundTriggerRecognitionStatusCallback callback = mIntentCallbacks.get(
- soundModelId.getUuid());
+ LocalSoundTriggerRecognitionStatusCallback callback = null;
+ synchronized (mCallbacksLock) {
+ callback = mIntentCallbacks.get(soundModelId.getUuid());
+ }
if (callback != null) {
Slog.e(TAG, soundModelId + " is already running");
return STATUS_ERROR;
@@ -291,7 +299,9 @@ public class SoundTriggerService extends SystemService {
Slog.e(TAG, "Failed to start model: " + ret);
return ret;
}
- mIntentCallbacks.put(soundModelId.getUuid(), callback);
+ synchronized (mCallbacksLock) {
+ mIntentCallbacks.put(soundModelId.getUuid(), callback);
+ }
}
return STATUS_OK;
}
@@ -310,8 +320,10 @@ public class SoundTriggerService extends SystemService {
Slog.e(TAG, soundModelId + " is not loaded");
return STATUS_ERROR;
}
- LocalSoundTriggerRecognitionStatusCallback callback = mIntentCallbacks.get(
- soundModelId.getUuid());
+ LocalSoundTriggerRecognitionStatusCallback callback = null;
+ synchronized (mCallbacksLock) {
+ callback = mIntentCallbacks.get(soundModelId.getUuid());
+ }
if (callback == null) {
Slog.e(TAG, soundModelId + " is not running");
return STATUS_ERROR;
@@ -334,7 +346,9 @@ public class SoundTriggerService extends SystemService {
Slog.e(TAG, "Failed to stop model: " + ret);
return ret;
}
- mIntentCallbacks.remove(soundModelId.getUuid());
+ synchronized (mCallbacksLock) {
+ mIntentCallbacks.remove(soundModelId.getUuid());
+ }
}
return STATUS_OK;
}
@@ -379,14 +393,14 @@ public class SoundTriggerService extends SystemService {
public boolean isRecognitionActive(ParcelUuid parcelUuid) {
enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
if (!isInitialized()) return false;
- synchronized (mLock) {
+ synchronized (mCallbacksLock) {
LocalSoundTriggerRecognitionStatusCallback callback =
mIntentCallbacks.get(parcelUuid.getUuid());
if (callback == null) {
return false;
}
- return mSoundTriggerHelper.isRecognitionRequested(parcelUuid.getUuid());
}
+ return mSoundTriggerHelper.isRecognitionRequested(parcelUuid.getUuid());
}
}
@@ -513,7 +527,7 @@ public class SoundTriggerService extends SystemService {
private void removeCallback(boolean releaseWakeLock) {
mCallbackIntent = null;
- synchronized (mLock) {
+ synchronized (mCallbacksLock) {
mIntentCallbacks.remove(mUuid);
if (releaseWakeLock) {
mWakelock.release();
@@ -523,7 +537,7 @@ public class SoundTriggerService extends SystemService {
}
private void grabWakeLock() {
- synchronized (mLock) {
+ synchronized (mCallbacksLock) {
if (mWakelock == null) {
PowerManager pm = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE));
mWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
@@ -537,7 +551,7 @@ public class SoundTriggerService extends SystemService {
public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
String resultData, Bundle resultExtras) {
// We're only ever invoked when the callback is done, so release the lock.
- synchronized (mLock) {
+ synchronized (mCallbacksLock) {
mWakelock.release();
}
}
diff --git a/com/android/server/stats/StatsCompanionService.java b/com/android/server/stats/StatsCompanionService.java
index e240ec5e..b31f4b3f 100644
--- a/com/android/server/stats/StatsCompanionService.java
+++ b/com/android/server/stats/StatsCompanionService.java
@@ -15,6 +15,7 @@
*/
package com.android.server.stats;
+import android.annotation.Nullable;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -25,16 +26,22 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.net.NetworkStats;
+import android.net.wifi.IWifiManager;
+import android.net.wifi.WifiActivityEnergyInfo;
+import android.telephony.ModemActivityInfo;
+import android.telephony.TelephonyManager;
import android.os.BatteryStatsInternal;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IStatsCompanionService;
import android.os.IStatsManager;
+import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.StatsLogEventWrapper;
+import android.os.SynchronousResultReceiver;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Slog;
@@ -52,6 +59,7 @@ import com.android.server.SystemService;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeoutException;
/**
* Helper service for statsd (the native stats management service in cmds/statsd/).
@@ -60,6 +68,13 @@ import java.util.Map;
* @hide
*/
public class StatsCompanionService extends IStatsCompanionService.Stub {
+ /**
+ * How long to wait on an individual subsystem to return its stats.
+ */
+ private static final long EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS = 2000;
+
+ public static final String RESULT_RECEIVER_CONTROLLER_KEY = "controller_activity";
+
static final String TAG = "StatsCompanionService";
static final boolean DEBUG = true;
public static final String ACTION_TRIGGER_COLLECTION =
@@ -79,6 +94,8 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
private final KernelWakelockReader mKernelWakelockReader = new KernelWakelockReader();
private final KernelWakelockStats mTmpWakelockStats = new KernelWakelockStats();
private final KernelCpuSpeedReader[] mKernelCpuSpeedReaders;
+ private IWifiManager mWifiManager = null;
+ private TelephonyManager mTelephony = null;
public StatsCompanionService(Context context) {
super();
@@ -138,6 +155,14 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
return ret;
}
+ private final static long[] toLongArray(List<Long> list) {
+ long[] ret = new long[list.size()];
+ for (int i = 0; i < ret.length; i++) {
+ ret[i] = list.get(i);
+ }
+ return ret;
+ }
+
// Assumes that sStatsdLock is held.
private final void informAllUidsLocked(Context context) throws RemoteException {
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
@@ -148,7 +173,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
}
List<Integer> uids = new ArrayList();
- List<Integer> versions = new ArrayList();
+ List<Long> versions = new ArrayList();
List<String> apps = new ArrayList();
// Add in all the apps for every user/profile.
@@ -157,12 +182,12 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
for (int j = 0; j < pi.size(); j++) {
if (pi.get(j).applicationInfo != null) {
uids.add(pi.get(j).applicationInfo.uid);
- versions.add(pi.get(j).versionCode);
+ versions.add(pi.get(j).getLongVersionCode());
apps.add(pi.get(j).packageName);
}
}
}
- sStatsd.informAllUidData(toIntArray(uids), toIntArray(versions), apps.toArray(new
+ sStatsd.informAllUidData(toIntArray(uids), toLongArray(versions), apps.toArray(new
String[apps.size()]));
if (DEBUG) {
Slog.w(TAG, "Sent data for " + uids.size() + " apps");
@@ -205,7 +230,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
int uid = b.getInt(Intent.EXTRA_UID);
String app = intent.getData().getSchemeSpecificPart();
PackageInfo pi = pm.getPackageInfo(app, PackageManager.MATCH_ANY_USER);
- sStatsd.informOnePackage(app, uid, pi.versionCode);
+ sStatsd.informOnePackage(app, uid, pi.getLongVersionCode());
}
} catch (Exception e) {
Slog.w(TAG, "Failed to inform statsd of an app update", e);
@@ -389,6 +414,40 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
return ret;
}
+ /**
+ * Helper method to extract the Parcelable controller info from a
+ * SynchronousResultReceiver.
+ */
+ private static <T extends Parcelable> T awaitControllerInfo(
+ @Nullable SynchronousResultReceiver receiver) {
+ if (receiver == null) {
+ return null;
+ }
+
+ try {
+ final SynchronousResultReceiver.Result result =
+ receiver.awaitResult(EXTERNAL_STATS_SYNC_TIMEOUT_MILLIS);
+ if (result.bundle != null) {
+ // This is the final destination for the Bundle.
+ result.bundle.setDefusable(true);
+
+ final T data = result.bundle.getParcelable(
+ RESULT_RECEIVER_CONTROLLER_KEY);
+ if (data != null) {
+ return data;
+ }
+ }
+ Slog.e(TAG, "no controller energy info supplied for " + receiver.getName());
+ } catch (TimeoutException e) {
+ Slog.w(TAG, "timeout reading " + receiver.getName() + " stats");
+ }
+ return null;
+ }
+
+ /**
+ *
+ * Pulls wifi controller activity energy info from WiFiManager
+ */
@Override // Binder call
public StatsLogEventWrapper[] pullData(int tagId) {
enforceCallingPermission();
@@ -396,7 +455,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
Slog.d(TAG, "Pulling " + tagId);
switch (tagId) {
- case StatsLog.WIFI_BYTES_TRANSFERRED: {
+ case StatsLog.WIFI_BYTES_TRANSFER: {
long token = Binder.clearCallingIdentity();
try {
// TODO: Consider caching the following call to get BatteryStatsInternal.
@@ -417,7 +476,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
}
break;
}
- case StatsLog.MOBILE_BYTES_TRANSFERRED: {
+ case StatsLog.MOBILE_BYTES_TRANSFER: {
long token = Binder.clearCallingIdentity();
try {
BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class);
@@ -437,7 +496,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
}
break;
}
- case StatsLog.WIFI_BYTES_TRANSFERRED_BY_FG_BG: {
+ case StatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG: {
long token = Binder.clearCallingIdentity();
try {
BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class);
@@ -457,7 +516,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
}
break;
}
- case StatsLog.MOBILE_BYTES_TRANSFERRED_BY_FG_BG: {
+ case StatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG: {
long token = Binder.clearCallingIdentity();
try {
BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class);
@@ -477,7 +536,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
}
break;
}
- case StatsLog.KERNEL_WAKELOCK_PULLED: {
+ case StatsLog.KERNEL_WAKELOCK: {
final KernelWakelockStats wakelockStats =
mKernelWakelockReader.readKernelWakelockStats(mTmpWakelockStats);
List<StatsLogEventWrapper> ret = new ArrayList();
@@ -493,7 +552,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
}
return ret.toArray(new StatsLogEventWrapper[ret.size()]);
}
- case StatsLog.CPU_TIME_PER_FREQ_PULLED: {
+ case StatsLog.CPU_TIME_PER_FREQ: {
List<StatsLogEventWrapper> ret = new ArrayList();
for (int cluster = 0; cluster < mKernelCpuSpeedReaders.length; cluster++) {
long[] clusterTimeMs = mKernelCpuSpeedReaders[cluster].readDelta();
@@ -509,6 +568,59 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
}
return ret.toArray(new StatsLogEventWrapper[ret.size()]);
}
+ case StatsLog.WIFI_ACTIVITY_ENERGY_INFO: {
+ List<StatsLogEventWrapper> ret = new ArrayList();
+ long token = Binder.clearCallingIdentity();
+ if (mWifiManager == null) {
+ mWifiManager = IWifiManager.Stub.asInterface(ServiceManager.getService(
+ Context.WIFI_SERVICE));
+ }
+ if (mWifiManager != null) {
+ try {
+ SynchronousResultReceiver wifiReceiver = new SynchronousResultReceiver("wifi");
+ mWifiManager.requestActivityInfo(wifiReceiver);
+ final WifiActivityEnergyInfo wifiInfo = awaitControllerInfo(wifiReceiver);
+ StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, 6);
+ e.writeLong(wifiInfo.getTimeStamp());
+ e.writeInt(wifiInfo.getStackState());
+ e.writeLong(wifiInfo.getControllerTxTimeMillis());
+ e.writeLong(wifiInfo.getControllerRxTimeMillis());
+ e.writeLong(wifiInfo.getControllerIdleTimeMillis());
+ e.writeLong(wifiInfo.getControllerEnergyUsed());
+ ret.add(e);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Pulling wifiManager for wifi controller activity energy info has error", e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ break;
+ }
+ case StatsLog.MODEM_ACTIVITY_INFO: {
+ List<StatsLogEventWrapper> ret = new ArrayList();
+ long token = Binder.clearCallingIdentity();
+ if (mTelephony == null) {
+ mTelephony = TelephonyManager.from(mContext);
+ }
+ if (mTelephony != null) {
+ SynchronousResultReceiver modemReceiver = new SynchronousResultReceiver("telephony");
+ mTelephony.requestModemActivityInfo(modemReceiver);
+ final ModemActivityInfo modemInfo = awaitControllerInfo(modemReceiver);
+ StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, 6);
+ e.writeLong(modemInfo.getTimestamp());
+ e.writeLong(modemInfo.getSleepTimeMillis());
+ e.writeLong(modemInfo.getIdleTimeMillis());
+ e.writeLong(modemInfo.getTxTimeMillis()[0]);
+ e.writeLong(modemInfo.getTxTimeMillis()[1]);
+ e.writeLong(modemInfo.getTxTimeMillis()[2]);
+ e.writeLong(modemInfo.getTxTimeMillis()[3]);
+ e.writeLong(modemInfo.getTxTimeMillis()[4]);
+ e.writeLong(modemInfo.getRxTimeMillis());
+ e.writeLong(modemInfo.getEnergyUsed());
+ ret.add(e);
+ }
+ break;
+ }
default:
Slog.w(TAG, "No such tagId data as " + tagId);
return null;
diff --git a/com/android/server/testing/FrameworkRobolectricTestRunner.java b/com/android/server/testing/FrameworkRobolectricTestRunner.java
new file mode 100644
index 00000000..6c7313ba
--- /dev/null
+++ b/com/android/server/testing/FrameworkRobolectricTestRunner.java
@@ -0,0 +1,157 @@
+/*
+ * 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 com.android.server.testing;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.internal.SandboxFactory;
+import org.robolectric.internal.SdkEnvironment;
+import org.robolectric.internal.bytecode.InstrumentationConfiguration;
+import org.robolectric.internal.bytecode.SandboxClassLoader;
+import org.robolectric.util.Util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+/**
+ * HACK
+ * Robolectric loads up Android environment from prebuilt android jars before running a method.
+ * These jars are versioned according to the SDK level configured for the method (or class). The
+ * jars represent a snapshot of the Android APIs in that SDK level. For Robolectric tests that are
+ * testing Android components themselves we don't want certain classes (usually the
+ * class-under-test) to be loaded from the prebuilt jar, we want it instead to be loaded from the
+ * dependencies of our test target, i.e. the system class loader. That way we can write tests
+ * against the actual classes that are in the tree, not a past version of them. Ideally we would
+ * have a locally built jar referenced by Robolectric, but until that happens one can use this
+ * class.
+ * This class reads the {@link SystemLoaderClasses} annotation on test classes and for each class
+ * in that annotation value it will bypass the android jar and load it from the system class loader.
+ * Allowing the test to test the actual class in the tree.
+ *
+ * Implementation note: One could think about overriding
+ * {@link RobolectricTestRunner#createClassLoaderConfig(FrameworkMethod)} method and putting the
+ * classes in the annotation in the {@link InstrumentationConfiguration} list of classes not to
+ * acquire. Unfortunately, this will not work because we will not be instrumenting the class.
+ * Instead, we have to load the class bytes from the system class loader but still instrument it, we
+ * do this by overriding {@link SandboxClassLoader#getByteCode(String)} and loading the class bytes
+ * from the system class loader if it in the {@link SystemLoaderClasses} annotation. This way the
+ * {@link SandboxClassLoader} still instruments the class, but it's not loaded from the android jar.
+ * Finally, we inject the custom class loader in place of the default one.
+ *
+ * TODO: Remove this when we are using locally built android jars in the method's environment.
+ */
+public class FrameworkRobolectricTestRunner extends RobolectricTestRunner {
+ private final SandboxFactory mSandboxFactory;
+
+ public FrameworkRobolectricTestRunner(Class<?> testClass) throws InitializationError {
+ super(testClass);
+ SystemLoaderClasses annotation = testClass.getAnnotation(SystemLoaderClasses.class);
+ Class<?>[] systemLoaderClasses =
+ (annotation != null) ? annotation.value() : new Class<?>[0];
+ Set<String> systemLoaderClassNames = classesToClassNames(systemLoaderClasses);
+ mSandboxFactory = new FrameworkSandboxFactory(systemLoaderClassNames);
+ }
+
+ @Nonnull
+ @Override
+ protected SdkEnvironment getSandbox(FrameworkMethod method) {
+ // HACK: Calling super just to get SdkConfig via sandbox.getSdkConfig(), because
+ // RobolectricFrameworkMethod, the runtime class of method, is package-protected
+ SdkEnvironment sandbox = super.getSandbox(method);
+ return mSandboxFactory.getSdkEnvironment(
+ createClassLoaderConfig(method),
+ getJarResolver(),
+ sandbox.getSdkConfig());
+ }
+
+ private static class FrameworkClassLoader extends SandboxClassLoader {
+ private final Set<String> mSystemLoaderClasses;
+
+ private FrameworkClassLoader(
+ Set<String> systemLoaderClasses,
+ ClassLoader systemClassLoader,
+ InstrumentationConfiguration instrumentationConfig,
+ URL... urls) {
+ super(systemClassLoader, instrumentationConfig, urls);
+ mSystemLoaderClasses = systemLoaderClasses;
+ }
+
+ @Override
+ protected byte[] getByteCode(String className) throws ClassNotFoundException {
+ String classFileName = className.replace('.', '/') + ".class";
+ if (shouldLoadFromSystemLoader(className)) {
+ try (InputStream classByteStream = getResourceAsStream(classFileName)) {
+ if (classByteStream == null) {
+ throw new ClassNotFoundException(className);
+ }
+ return Util.readBytes(classByteStream);
+ } catch (IOException e) {
+ throw new ClassNotFoundException(
+ "Couldn't load " + className + " from system class loader", e);
+ }
+ }
+ return super.getByteCode(className);
+ }
+
+ /**
+ * Classes like com.package.ClassName$InnerClass should also be loaded from the system class
+ * loader, so we test if the classes in the annotation are prefixes of the class to load.
+ */
+ private boolean shouldLoadFromSystemLoader(String className) {
+ for (String classNamePrefix : mSystemLoaderClasses) {
+ if (className.startsWith(classNamePrefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private static class FrameworkSandboxFactory extends SandboxFactory {
+ private final Set<String> mSystemLoaderClasses;
+
+ private FrameworkSandboxFactory(Set<String> systemLoaderClasses) {
+ mSystemLoaderClasses = systemLoaderClasses;
+ }
+
+ @Nonnull
+ @Override
+ public ClassLoader createClassLoader(
+ InstrumentationConfiguration instrumentationConfig, URL... urls) {
+ return new FrameworkClassLoader(
+ mSystemLoaderClasses,
+ ClassLoader.getSystemClassLoader(),
+ instrumentationConfig,
+ urls);
+ }
+ }
+
+ private static Set<String> classesToClassNames(Class<?>[] classes) {
+ ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+ for (Class<?> classObject : classes) {
+ builder.add(classObject.getName());
+ }
+ return builder.build();
+ }
+}
diff --git a/com/android/server/testing/SystemLoaderClasses.java b/com/android/server/testing/SystemLoaderClasses.java
new file mode 100644
index 00000000..646a413c
--- /dev/null
+++ b/com/android/server/testing/SystemLoaderClasses.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.android.server.testing;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to be used in test classes that run with {@link FrameworkRobolectricTestRunner}.
+ * This will make the classes specified be loaded from the system class loader, NOT from the
+ * Robolectric android jar.
+ *
+ * @see FrameworkRobolectricTestRunner
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SystemLoaderClasses {
+ Class<?>[] value() default {};
+}
diff --git a/com/android/server/timezone/CheckToken.java b/com/android/server/timezone/CheckToken.java
index 51283608..4c4a8d72 100644
--- a/com/android/server/timezone/CheckToken.java
+++ b/com/android/server/timezone/CheckToken.java
@@ -46,8 +46,8 @@ final class CheckToken {
ByteArrayOutputStream baos = new ByteArrayOutputStream(12 /* (3 * sizeof(int)) */);
try (DataOutputStream dos = new DataOutputStream(baos)) {
dos.writeInt(mOptimisticLockId);
- dos.writeInt(mPackageVersions.mUpdateAppVersion);
- dos.writeInt(mPackageVersions.mDataAppVersion);
+ dos.writeLong(mPackageVersions.mUpdateAppVersion);
+ dos.writeLong(mPackageVersions.mDataAppVersion);
} catch (IOException e) {
throw new RuntimeException("Unable to write into a ByteArrayOutputStream", e);
}
@@ -58,8 +58,8 @@ final class CheckToken {
ByteArrayInputStream bais = new ByteArrayInputStream(tokenBytes);
try (DataInputStream dis = new DataInputStream(bais)) {
int versionId = dis.readInt();
- int updateAppVersion = dis.readInt();
- int dataAppVersion = dis.readInt();
+ long updateAppVersion = dis.readLong();
+ long dataAppVersion = dis.readLong();
return new CheckToken(versionId, new PackageVersions(updateAppVersion, dataAppVersion));
}
}
diff --git a/com/android/server/timezone/PackageManagerHelper.java b/com/android/server/timezone/PackageManagerHelper.java
index 804941ad..f6e35e8f 100644
--- a/com/android/server/timezone/PackageManagerHelper.java
+++ b/com/android/server/timezone/PackageManagerHelper.java
@@ -26,7 +26,7 @@ import android.content.pm.PackageManager;
*/
interface PackageManagerHelper {
- int getInstalledPackageVersion(String packageName)
+ long getInstalledPackageVersion(String packageName)
throws PackageManager.NameNotFoundException;
boolean isPrivilegedApp(String packageName) throws PackageManager.NameNotFoundException;
diff --git a/com/android/server/timezone/PackageStatusStorage.java b/com/android/server/timezone/PackageStatusStorage.java
index cac7f7b8..251a2776 100644
--- a/com/android/server/timezone/PackageStatusStorage.java
+++ b/com/android/server/timezone/PackageStatusStorage.java
@@ -76,18 +76,22 @@ final class PackageStatusStorage {
*/
private static final String ATTRIBUTE_DATA_APP_VERSION = "dataAppPackageVersion";
- private static final int UNKNOWN_PACKAGE_VERSION = -1;
+ private static final long UNKNOWN_PACKAGE_VERSION = -1;
private final AtomicFile mPackageStatusFile;
PackageStatusStorage(File storageDir) {
mPackageStatusFile = new AtomicFile(new File(storageDir, "package-status.xml"));
+ }
+
+ /**
+ * Initialize any storage, as needed.
+ *
+ * @throws IOException if the storage could not be initialized
+ */
+ void initialize() throws IOException {
if (!mPackageStatusFile.getBaseFile().exists()) {
- try {
- insertInitialPackageStatus();
- } catch (IOException e) {
- throw new IllegalStateException(e);
- }
+ insertInitialPackageStatus();
}
}
@@ -320,14 +324,14 @@ final class PackageStatusStorage {
serializer.attribute(namespace, ATTRIBUTE_CHECK_STATUS, statusAttributeValue);
serializer.attribute(namespace, ATTRIBUTE_OPTIMISTIC_LOCK_ID,
Integer.toString(optimisticLockId));
- int updateAppVersion = status == null
+ long updateAppVersion = status == null
? UNKNOWN_PACKAGE_VERSION : packageVersions.mUpdateAppVersion;
serializer.attribute(namespace, ATTRIBUTE_UPDATE_APP_VERSION,
- Integer.toString(updateAppVersion));
- int dataAppVersion = status == null
+ Long.toString(updateAppVersion));
+ long dataAppVersion = status == null
? UNKNOWN_PACKAGE_VERSION : packageVersions.mDataAppVersion;
serializer.attribute(namespace, ATTRIBUTE_DATA_APP_VERSION,
- Integer.toString(dataAppVersion));
+ Long.toString(dataAppVersion));
serializer.endTag(namespace, TAG_PACKAGE_STATUS);
serializer.endDocument();
serializer.flush();
@@ -342,13 +346,13 @@ final class PackageStatusStorage {
}
/** Only used during tests to force a known table state. */
- public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) {
+ public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions)
+ throws IOException {
synchronized (this) {
try {
- int optimisticLockId = getCurrentOptimisticLockId();
- writePackageStatusWithOptimisticLockCheck(optimisticLockId, optimisticLockId,
- checkStatus, packageVersions);
- } catch (IOException | ParseException e) {
+ final int initialOptimisticLockId = (int) System.currentTimeMillis();
+ writePackageStatusLocked(checkStatus, initialOptimisticLockId, packageVersions);
+ } catch (IOException e) {
throw new IllegalStateException(e);
}
}
diff --git a/com/android/server/timezone/PackageTracker.java b/com/android/server/timezone/PackageTracker.java
index f0306b9b..0e8d8bc8 100644
--- a/com/android/server/timezone/PackageTracker.java
+++ b/com/android/server/timezone/PackageTracker.java
@@ -22,11 +22,15 @@ import android.app.timezone.RulesUpdaterContract;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
+import android.os.FileUtils;
+import android.os.SystemClock;
import android.provider.TimeZoneRulesDataContract;
import android.util.Slog;
import java.io.File;
+import java.io.IOException;
import java.io.PrintWriter;
+import java.time.Clock;
/**
* Monitors the installed applications associated with time zone updates. If the app packages are
@@ -58,7 +62,7 @@ public class PackageTracker {
private final IntentHelper mIntentHelper;
private final ConfigHelper mConfigHelper;
private final PackageStatusStorage mPackageStatusStorage;
- private final ClockHelper mClockHelper;
+ private final Clock mElapsedRealtimeClock;
// False if tracking is disabled.
private boolean mTrackingEnabled;
@@ -91,15 +95,11 @@ public class PackageTracker {
/** Creates the {@link PackageTracker} for normal use. */
static PackageTracker create(Context context) {
+ Clock elapsedRealtimeClock = SystemClock.elapsedRealtimeClock();
PackageTrackerHelperImpl helperImpl = new PackageTrackerHelperImpl(context);
- // TODO(nfuller): Switch to FileUtils.createDir() when available. http://b/31008728
- File storageDir = new File(Environment.getDataSystemDirectory(), "timezone");
- if (!storageDir.exists()) {
- storageDir.mkdir();
- }
-
+ File storageDir = FileUtils.createDir(Environment.getDataSystemDirectory(), "timezone");
return new PackageTracker(
- helperImpl /* clock */,
+ elapsedRealtimeClock /* elapsedRealtimeClock */,
helperImpl /* configHelper */,
helperImpl /* packageManagerHelper */,
new PackageStatusStorage(storageDir),
@@ -107,10 +107,10 @@ public class PackageTracker {
}
// A constructor that can be used by tests to supply mocked / faked dependencies.
- PackageTracker(ClockHelper clockHelper, ConfigHelper configHelper,
+ PackageTracker(Clock elapsedRealtimeClock, ConfigHelper configHelper,
PackageManagerHelper packageManagerHelper, PackageStatusStorage packageStatusStorage,
IntentHelper intentHelper) {
- mClockHelper = clockHelper;
+ mElapsedRealtimeClock = elapsedRealtimeClock;
mConfigHelper = configHelper;
mPackageManagerHelper = packageManagerHelper;
mPackageStatusStorage = packageStatusStorage;
@@ -118,11 +118,11 @@ public class PackageTracker {
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
- protected synchronized void start() {
+ protected synchronized boolean start() {
mTrackingEnabled = mConfigHelper.isTrackingEnabled();
if (!mTrackingEnabled) {
Slog.i(TAG, "Time zone updater / data package tracking explicitly disabled.");
- return;
+ return false;
}
mUpdateAppPackageName = mConfigHelper.getUpdateAppPackageName();
@@ -140,6 +140,14 @@ public class PackageTracker {
mCheckTriggered = false;
mCheckFailureCount = 0;
+ // Initialize the storage, as needed.
+ try {
+ mPackageStatusStorage.initialize();
+ } catch (IOException e) {
+ Slog.w(TAG, "PackageTracker storage could not be initialized.", e);
+ return false;
+ }
+
// Initialize the intent helper.
mIntentHelper.initialize(mUpdateAppPackageName, mDataAppPackageName, this);
@@ -149,6 +157,7 @@ public class PackageTracker {
mIntentHelper.scheduleReliabilityTrigger(mDelayBeforeReliabilityCheckMillis);
Slog.i(TAG, "Time zone updater / data package tracking enabled");
+ return true;
}
/**
@@ -425,7 +434,7 @@ public class PackageTracker {
}
private void setCheckInProgress() {
- mLastTriggerTimestamp = mClockHelper.currentTimestamp();
+ mLastTriggerTimestamp = mElapsedRealtimeClock.millis();
}
private void setCheckComplete() {
@@ -441,12 +450,12 @@ public class PackageTracker {
return false;
}
// Risk of overflow, but highly unlikely given the implementation and not problematic.
- return mClockHelper.currentTimestamp() > mLastTriggerTimestamp + mCheckTimeAllowedMillis;
+ return mElapsedRealtimeClock.millis() > mLastTriggerTimestamp + mCheckTimeAllowedMillis;
}
private PackageVersions lookupInstalledPackageVersions() {
- int updatePackageVersion;
- int dataPackageVersion;
+ long updatePackageVersion;
+ long dataPackageVersion;
try {
updatePackageVersion =
mPackageManagerHelper.getInstalledPackageVersion(mUpdateAppPackageName);
diff --git a/com/android/server/timezone/PackageTrackerHelperImpl.java b/com/android/server/timezone/PackageTrackerHelperImpl.java
index b89dd383..5f90be13 100644
--- a/com/android/server/timezone/PackageTrackerHelperImpl.java
+++ b/com/android/server/timezone/PackageTrackerHelperImpl.java
@@ -25,7 +25,6 @@ import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
-import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Slog;
@@ -34,7 +33,7 @@ import java.util.List;
/**
* A single class that implements multiple helper interfaces for use by {@link PackageTracker}.
*/
-final class PackageTrackerHelperImpl implements ClockHelper, ConfigHelper, PackageManagerHelper {
+final class PackageTrackerHelperImpl implements ConfigHelper, PackageManagerHelper {
private static final String TAG = "PackageTrackerHelperImpl";
@@ -74,18 +73,11 @@ final class PackageTrackerHelperImpl implements ClockHelper, ConfigHelper, Packa
}
@Override
- public long currentTimestamp() {
- // Use of elapsedRealtime() because this is in-memory state and elapsedRealtime() shouldn't
- // change if the system clock changes.
- return SystemClock.elapsedRealtime();
- }
-
- @Override
- public int getInstalledPackageVersion(String packageName)
+ public long getInstalledPackageVersion(String packageName)
throws PackageManager.NameNotFoundException {
int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName, flags);
- return packageInfo.versionCode;
+ return packageInfo.getLongVersionCode();
}
@Override
diff --git a/com/android/server/timezone/PackageVersions.java b/com/android/server/timezone/PackageVersions.java
index fc0d6e1a..0084c1a2 100644
--- a/com/android/server/timezone/PackageVersions.java
+++ b/com/android/server/timezone/PackageVersions.java
@@ -21,10 +21,10 @@ package com.android.server.timezone;
*/
final class PackageVersions {
- final int mUpdateAppVersion;
- final int mDataAppVersion;
+ final long mUpdateAppVersion;
+ final long mDataAppVersion;
- PackageVersions(int updateAppVersion, int dataAppVersion) {
+ PackageVersions(long updateAppVersion, long dataAppVersion) {
this.mUpdateAppVersion = updateAppVersion;
this.mDataAppVersion = dataAppVersion;
}
@@ -48,8 +48,8 @@ final class PackageVersions {
@Override
public int hashCode() {
- int result = mUpdateAppVersion;
- result = 31 * result + mDataAppVersion;
+ int result = Long.hashCode(mUpdateAppVersion);
+ result = 31 * result + Long.hashCode(mDataAppVersion);
return result;
}
diff --git a/com/android/server/timezone/RulesManagerService.java b/com/android/server/timezone/RulesManagerService.java
index 52b49baf..30fc63c2 100644
--- a/com/android/server/timezone/RulesManagerService.java
+++ b/com/android/server/timezone/RulesManagerService.java
@@ -121,6 +121,7 @@ public final class RulesManagerService extends IRulesManager.Stub {
}
public void start() {
+ // Return value deliberately ignored: no action required on failure to start.
mPackageTracker.start();
}
diff --git a/com/android/server/timezone/RulesManagerServiceHelperImpl.java b/com/android/server/timezone/RulesManagerServiceHelperImpl.java
index 0cf61c0c..e8a401e7 100644
--- a/com/android/server/timezone/RulesManagerServiceHelperImpl.java
+++ b/com/android/server/timezone/RulesManagerServiceHelperImpl.java
@@ -16,6 +16,8 @@
package com.android.server.timezone;
+import com.android.internal.util.DumpUtils;
+
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
@@ -46,15 +48,7 @@ final class RulesManagerServiceHelperImpl implements PermissionHelper, Executor
@Override
public boolean checkDumpPermission(String tag, PrintWriter pw) {
- // TODO(nfuller): Switch to DumpUtils.checkDumpPermission() when it is available in AOSP.
- if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
- != PackageManager.PERMISSION_GRANTED) {
- pw.println("Permission Denial: can't dump LocationManagerService from from pid="
- + Binder.getCallingPid()
- + ", uid=" + Binder.getCallingUid());
- return false;
- }
- return true;
+ return DumpUtils.checkDumpPermission(mContext, tag, pw);
}
@Override
diff --git a/com/android/server/trust/TrustAgentWrapper.java b/com/android/server/trust/TrustAgentWrapper.java
index 13828946..ca0a4505 100644
--- a/com/android/server/trust/TrustAgentWrapper.java
+++ b/com/android/server/trust/TrustAgentWrapper.java
@@ -512,7 +512,7 @@ public class TrustAgentWrapper {
} else {
mTrustAgentService.onConfigure(Collections.EMPTY_LIST, null);
}
- final long maxTimeToLock = dpm.getMaximumTimeToLockForUserAndProfiles(mUserId);
+ final long maxTimeToLock = dpm.getMaximumTimeToLock(null, mUserId);
if (maxTimeToLock != mMaximumTimeToLock) {
// If the timeout changes, cancel the alarm and send a timeout event to have
// the agent re-evaluate trust.
diff --git a/com/android/server/usage/AppIdleHistory.java b/com/android/server/usage/AppIdleHistory.java
index ee112415..a1f18106 100644
--- a/com/android/server/usage/AppIdleHistory.java
+++ b/com/android/server/usage/AppIdleHistory.java
@@ -16,10 +16,16 @@
package com.android.server.usage;
-import static android.app.usage.AppStandby.*;
-
-import android.app.usage.AppStandby;
-import android.os.Environment;
+import static android.app.usage.UsageStatsManager.REASON_DEFAULT;
+import static android.app.usage.UsageStatsManager.REASON_FORCED;
+import static android.app.usage.UsageStatsManager.REASON_PREDICTED;
+import static android.app.usage.UsageStatsManager.REASON_USAGE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
+
+import android.app.usage.UsageStatsManager;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.AtomicFile;
@@ -45,6 +51,8 @@ import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
/**
* Keeps track of recent active state changes in apps.
@@ -75,14 +83,13 @@ public class AppIdleHistory {
private static final String ATTR_SCREEN_IDLE = "screenIdleTime";
// Elapsed timebase time when app was last used
private static final String ATTR_ELAPSED_IDLE = "elapsedIdleTime";
+ // Elapsed timebase time when the app bucket was last predicted externally
+ private static final String ATTR_LAST_PREDICTED_TIME = "lastPredictedTime";
+ // The standby bucket for the app
private static final String ATTR_CURRENT_BUCKET = "appLimitBucket";
+ // The reason the app was put in the above bucket
private static final String ATTR_BUCKETING_REASON = "bucketReason";
- // State that was last informed to listeners, since boot
- private static final int STATE_UNINFORMED = 0;
- private static final int STATE_ACTIVE = 1;
- private static final int STATE_IDLE = 2;
-
// device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
private long mElapsedDuration; // Total device on duration since device was "born"
@@ -95,12 +102,21 @@ public class AppIdleHistory {
private boolean mScreenOn;
- private static class AppUsageHistory {
+ static class AppUsageHistory {
+ // Debug
final byte[] recent = new byte[HISTORY_SIZE];
+ // Last used time using elapsed timebase
long lastUsedElapsedTime;
+ // Last used time using screen_on timebase
long lastUsedScreenTime;
- @StandbyBuckets int currentBucket;
+ // Last predicted time using elapsed timebase
+ long lastPredictedTime;
+ // Standby bucket
+ @UsageStatsManager.StandbyBuckets
+ int currentBucket;
+ // Reason for setting the standby bucket. TODO: Switch to int.
String bucketingReason;
+ // In-memory only, last bucket for which the listeners were informed
int lastInformedBucket;
}
@@ -197,7 +213,7 @@ public class AppIdleHistory {
+ ", reason=" + appUsageHistory.bucketingReason);
}
}
- appUsageHistory.bucketingReason = AppStandby.REASON_USAGE;
+ appUsageHistory.bucketingReason = REASON_USAGE;
return appUsageHistory.currentBucket;
}
@@ -214,7 +230,7 @@ public class AppIdleHistory {
}
}
// TODO: Should this be a different reason for partial usage?
- appUsageHistory.bucketingReason = AppStandby.REASON_USAGE;
+ appUsageHistory.bucketingReason = REASON_USAGE;
return appUsageHistory.currentBucket;
}
@@ -271,8 +287,10 @@ public class AppIdleHistory {
appUsageHistory = new AppUsageHistory();
appUsageHistory.lastUsedElapsedTime = getElapsedTime(elapsedRealtime);
appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
- appUsageHistory.currentBucket = AppStandby.STANDBY_BUCKET_NEVER;
+ appUsageHistory.lastPredictedTime = getElapsedTime(0);
+ appUsageHistory.currentBucket = STANDBY_BUCKET_NEVER;
appUsageHistory.bucketingReason = REASON_DEFAULT;
+ appUsageHistory.lastInformedBucket = -1;
userHistory.put(packageName, appUsageHistory);
}
return appUsageHistory;
@@ -289,13 +307,21 @@ public class AppIdleHistory {
if (appUsageHistory == null) {
return false; // Default to not idle
} else {
- return appUsageHistory.currentBucket >= AppStandby.STANDBY_BUCKET_RARE;
+ return appUsageHistory.currentBucket >= STANDBY_BUCKET_RARE;
// Whether or not it's passed will now be externally calculated and the
// bucket will be pushed to the history using setAppStandbyBucket()
//return hasPassedThresholds(appUsageHistory, elapsedRealtime);
}
}
+ public AppUsageHistory getAppUsageHistory(String packageName, int userId,
+ long elapsedRealtime) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, true);
+ return appUsageHistory;
+ }
+
public void setAppStandbyBucket(String packageName, int userId, long elapsedRealtime,
int bucket, String reason) {
ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
@@ -303,6 +329,9 @@ public class AppIdleHistory {
getPackageHistory(userHistory, packageName, elapsedRealtime, true);
appUsageHistory.currentBucket = bucket;
appUsageHistory.bucketingReason = reason;
+ if (reason.startsWith(REASON_PREDICTED)) {
+ appUsageHistory.lastPredictedTime = getElapsedTime(elapsedRealtime);
+ }
if (DEBUG) {
Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory.currentBucket
+ ", reason=" + appUsageHistory.bucketingReason);
@@ -316,6 +345,18 @@ public class AppIdleHistory {
return appUsageHistory.currentBucket;
}
+ public Map<String, Integer> getAppStandbyBuckets(int userId, long elapsedRealtime,
+ boolean appIdleEnabled) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ int size = userHistory.size();
+ HashMap<String, Integer> buckets = new HashMap<>(size);
+ for (int i = 0; i < size; i++) {
+ buckets.put(userHistory.keyAt(i),
+ appIdleEnabled ? userHistory.valueAt(i).currentBucket : STANDBY_BUCKET_ACTIVE);
+ }
+ return buckets;
+ }
+
public String getAppStandbyReason(String packageName, int userId, long elapsedRealtime) {
ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
AppUsageHistory appUsageHistory =
@@ -323,7 +364,7 @@ public class AppIdleHistory {
return appUsageHistory != null ? appUsageHistory.bucketingReason : null;
}
- private long getElapsedTime(long elapsedRealtime) {
+ public long getElapsedTime(long elapsedRealtime) {
return (elapsedRealtime - mElapsedSnapshot + mElapsedDuration);
}
@@ -432,16 +473,23 @@ public class AppIdleHistory {
Long.parseLong(parser.getAttributeValue(null, ATTR_ELAPSED_IDLE));
appUsageHistory.lastUsedScreenTime =
Long.parseLong(parser.getAttributeValue(null, ATTR_SCREEN_IDLE));
+ String lastPredictedTimeString = parser.getAttributeValue(null,
+ ATTR_LAST_PREDICTED_TIME);
+ if (lastPredictedTimeString != null) {
+ appUsageHistory.lastPredictedTime =
+ Long.parseLong(lastPredictedTimeString);
+ }
String currentBucketString = parser.getAttributeValue(null,
ATTR_CURRENT_BUCKET);
appUsageHistory.currentBucket = currentBucketString == null
- ? AppStandby.STANDBY_BUCKET_ACTIVE
+ ? STANDBY_BUCKET_ACTIVE
: Integer.parseInt(currentBucketString);
appUsageHistory.bucketingReason =
parser.getAttributeValue(null, ATTR_BUCKETING_REASON);
if (appUsageHistory.bucketingReason == null) {
appUsageHistory.bucketingReason = REASON_DEFAULT;
}
+ appUsageHistory.lastInformedBucket = -1;
userHistory.put(packageName, appUsageHistory);
}
}
@@ -478,6 +526,8 @@ public class AppIdleHistory {
Long.toString(history.lastUsedElapsedTime));
xml.attribute(null, ATTR_SCREEN_IDLE,
Long.toString(history.lastUsedScreenTime));
+ xml.attribute(null, ATTR_LAST_PREDICTED_TIME,
+ Long.toString(history.lastPredictedTime));
xml.attribute(null, ATTR_CURRENT_BUCKET,
Integer.toString(history.currentBucket));
xml.attribute(null, ATTR_BUCKETING_REASON, history.bucketingReason);
@@ -513,6 +563,8 @@ public class AppIdleHistory {
TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastUsedElapsedTime, idpw);
idpw.print(" lastUsedScreenOn=");
TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw);
+ idpw.print(" lastPredictedTime=");
+ TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw);
idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
idpw.print(" bucket=" + appUsageHistory.currentBucket
+ " reason=" + appUsageHistory.bucketingReason);
diff --git a/com/android/server/usage/AppStandbyController.java b/com/android/server/usage/AppStandbyController.java
index 3c099c24..9b588fa3 100644
--- a/com/android/server/usage/AppStandbyController.java
+++ b/com/android/server/usage/AppStandbyController.java
@@ -16,15 +16,27 @@
package com.android.server.usage;
+import static android.app.usage.UsageStatsManager.REASON_DEFAULT;
+import static android.app.usage.UsageStatsManager.REASON_FORCED;
+import static android.app.usage.UsageStatsManager.REASON_PREDICTED;
+import static android.app.usage.UsageStatsManager.REASON_TIMEOUT;
+import static android.app.usage.UsageStatsManager.REASON_USAGE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
+import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
+
import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.admin.DevicePolicyManager;
-import android.app.usage.AppStandby;
-import android.app.usage.AppStandby.StandbyBuckets;
+import android.app.usage.UsageStatsManager.StandbyBuckets;
import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
@@ -73,6 +85,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
/**
* Manages the standby state of an app, listening to various events.
@@ -102,12 +115,15 @@ public class AppStandbyController {
};
static final int[] THRESHOLD_BUCKETS = {
- AppStandby.STANDBY_BUCKET_ACTIVE,
- AppStandby.STANDBY_BUCKET_WORKING_SET,
- AppStandby.STANDBY_BUCKET_FREQUENT,
- AppStandby.STANDBY_BUCKET_RARE
+ STANDBY_BUCKET_ACTIVE,
+ STANDBY_BUCKET_WORKING_SET,
+ STANDBY_BUCKET_FREQUENT,
+ STANDBY_BUCKET_RARE
};
+ // Expiration time for predicted bucket
+ private static final long PREDICTION_TIMEOUT = 12 * ONE_HOUR;
+
// To name the lock for stack traces
static class Lock {}
@@ -118,7 +134,7 @@ public class AppStandbyController {
@GuardedBy("mAppIdleLock")
private AppIdleHistory mAppIdleHistory;
- @GuardedBy("mAppIdleLock")
+ @GuardedBy("mPackageAccessListeners")
private ArrayList<AppIdleStateChangeListener>
mPackageAccessListeners = new ArrayList<>();
@@ -146,12 +162,14 @@ public class AppStandbyController {
long[] mAppStandbyScreenThresholds = SCREEN_TIME_THRESHOLDS;
long[] mAppStandbyElapsedThresholds = ELAPSED_TIME_THRESHOLDS;
- boolean mAppIdleEnabled;
+ volatile boolean mAppIdleEnabled;
boolean mAppIdleTempParoled;
boolean mCharging;
private long mLastAppIdleParoledTime;
private boolean mSystemServicesReady = false;
+ private final DeviceStateReceiver mDeviceStateReceiver;
+
private volatile boolean mPendingOneTimeCheckIdleStates;
private final AppStandbyHandler mHandler;
@@ -162,7 +180,7 @@ public class AppStandbyController {
private AppWidgetManager mAppWidgetManager;
private PowerManager mPowerManager;
private PackageManager mPackageManager;
- private Injector mInjector;
+ Injector mInjector;
AppStandbyController(Context context, Looper looper) {
@@ -174,14 +192,13 @@ public class AppStandbyController {
mContext = mInjector.getContext();
mHandler = new AppStandbyHandler(mInjector.getLooper());
mPackageManager = mContext.getPackageManager();
- mAppIdleEnabled = mInjector.isAppIdleEnabled();
+ mDeviceStateReceiver = new DeviceStateReceiver();
+
+ IntentFilter deviceStates = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+ deviceStates.addAction(BatteryManager.ACTION_DISCHARGING);
+ deviceStates.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
+ mContext.registerReceiver(mDeviceStateReceiver, deviceStates);
- if (mAppIdleEnabled) {
- IntentFilter deviceStates = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
- deviceStates.addAction(BatteryManager.ACTION_DISCHARGING);
- deviceStates.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
- mContext.registerReceiver(new DeviceStateReceiver(), deviceStates);
- }
synchronized (mAppIdleLock) {
mAppIdleHistory = new AppIdleHistory(mInjector.getDataSystemDirectory(),
mInjector.elapsedRealtime());
@@ -197,9 +214,14 @@ public class AppStandbyController {
null, mHandler);
}
+ void setAppIdleEnabled(boolean enabled) {
+ mAppIdleEnabled = enabled;
+ }
+
public void onBootPhase(int phase) {
mInjector.onBootPhase(phase);
if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ setAppIdleEnabled(mInjector.isAppIdleEnabled());
// Observe changes to the threshold
SettingsObserver settingsObserver = new SettingsObserver(mHandler);
settingsObserver.registerObserver();
@@ -224,6 +246,8 @@ public class AppStandbyController {
}
void reportContentProviderUsage(String authority, String providerPkgName, int userId) {
+ if (!mAppIdleEnabled) return;
+
// Get sync adapters for the authority
String[] packages = ContentResolver.getSyncAdapterPackagesForAuthorityAsUser(
authority, userId);
@@ -279,6 +303,7 @@ public class AppStandbyController {
}
boolean isParoledOrCharging() {
+ if (!mAppIdleEnabled) return true;
synchronized (mAppIdleLock) {
return mAppIdleTempParoled || mCharging;
}
@@ -370,30 +395,37 @@ public class AppStandbyController {
Slog.d(TAG, " Checking idle state for " + packageName);
}
if (isSpecial) {
+ synchronized (mAppIdleLock) {
+ mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
+ STANDBY_BUCKET_EXEMPTED, REASON_DEFAULT);
+ }
maybeInformListeners(packageName, userId, elapsedRealtime,
- AppStandby.STANDBY_BUCKET_ACTIVE);
+ STANDBY_BUCKET_EXEMPTED);
} else {
synchronized (mAppIdleLock) {
- String bucketingReason = mAppIdleHistory.getAppStandbyReason(packageName,
+ AppIdleHistory.AppUsageHistory app =
+ mAppIdleHistory.getAppUsageHistory(packageName,
userId, elapsedRealtime);
// If the bucket was forced by the developer, leave it alone
- if (AppStandby.REASON_FORCED.equals(bucketingReason)) {
+ if (REASON_FORCED.equals(app.bucketingReason)) {
continue;
}
+ boolean predictionLate = false;
// If the bucket was moved up due to usage, let the timeouts apply.
- if (AppStandby.REASON_USAGE.equals(bucketingReason)
- || AppStandby.REASON_TIMEOUT.equals(bucketingReason)) {
- int oldBucket = mAppIdleHistory.getAppStandbyBucket(packageName, userId,
- elapsedRealtime);
+ if (REASON_DEFAULT.equals(app.bucketingReason)
+ || REASON_USAGE.equals(app.bucketingReason)
+ || REASON_TIMEOUT.equals(app.bucketingReason)
+ || (predictionLate = predictionTimedOut(app, elapsedRealtime))) {
+ int oldBucket = app.currentBucket;
int newBucket = getBucketForLocked(packageName, userId,
elapsedRealtime);
if (DEBUG) {
Slog.d(TAG, " Old bucket=" + oldBucket
+ ", newBucket=" + newBucket);
}
- if (oldBucket < newBucket) {
+ if (oldBucket < newBucket || predictionLate) {
mAppIdleHistory.setAppStandbyBucket(packageName, userId,
- elapsedRealtime, newBucket, AppStandby.REASON_TIMEOUT);
+ elapsedRealtime, newBucket, REASON_TIMEOUT);
maybeInformListeners(packageName, userId, elapsedRealtime,
newBucket);
}
@@ -409,6 +441,14 @@ public class AppStandbyController {
return true;
}
+ private boolean predictionTimedOut(AppIdleHistory.AppUsageHistory app, long elapsedRealtime) {
+ return app.bucketingReason != null
+ && app.bucketingReason.startsWith(REASON_PREDICTED)
+ && app.lastPredictedTime > 0
+ && mAppIdleHistory.getElapsedTime(elapsedRealtime)
+ - app.lastPredictedTime > PREDICTION_TIMEOUT;
+ }
+
private void maybeInformListeners(String packageName, int userId,
long elapsedRealtime, int bucket) {
synchronized (mAppIdleLock) {
@@ -489,6 +529,7 @@ public class AppStandbyController {
}
void reportEvent(UsageEvents.Event event, long elapsedRealtime, int userId) {
+ if (!mAppIdleEnabled) return;
synchronized (mAppIdleLock) {
// TODO: Ideally this should call isAppIdleFiltered() to avoid calling back
// about apps that are on some kind of whitelist anyway.
@@ -528,6 +569,8 @@ public class AppStandbyController {
* required.
*/
void forceIdleState(String packageName, int userId, boolean idle) {
+ if (!mAppIdleEnabled) return;
+
final int appId = getAppId(packageName);
if (appId < 0) return;
final long elapsedRealtime = mInjector.elapsedRealtime();
@@ -562,7 +605,7 @@ public class AppStandbyController {
}
void addListener(AppIdleStateChangeListener listener) {
- synchronized (mAppIdleLock) {
+ synchronized (mPackageAccessListeners) {
if (!mPackageAccessListeners.contains(listener)) {
mPackageAccessListeners.add(listener);
}
@@ -570,7 +613,7 @@ public class AppStandbyController {
}
void removeListener(AppIdleStateChangeListener listener) {
- synchronized (mAppIdleLock) {
+ synchronized (mPackageAccessListeners) {
mPackageAccessListeners.remove(listener);
}
}
@@ -732,7 +775,7 @@ public class AppStandbyController {
}
void setAppIdleAsync(String packageName, boolean idle, int userId) {
- if (packageName == null) return;
+ if (packageName == null || !mAppIdleEnabled) return;
mHandler.obtainMessage(MSG_FORCE_IDLE_STATE, userId, idle ? 1 : 0, packageName)
.sendToTarget();
@@ -740,18 +783,41 @@ public class AppStandbyController {
@StandbyBuckets public int getAppStandbyBucket(String packageName, int userId,
long elapsedRealtime, boolean shouldObfuscateInstantApps) {
- if (shouldObfuscateInstantApps &&
- mInjector.isPackageEphemeral(userId, packageName)) {
- return AppStandby.STANDBY_BUCKET_ACTIVE;
+ if (!mAppIdleEnabled || (shouldObfuscateInstantApps
+ && mInjector.isPackageEphemeral(userId, packageName))) {
+ return STANDBY_BUCKET_ACTIVE;
}
- return mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime);
+ synchronized (mAppIdleLock) {
+ return mAppIdleHistory.getAppStandbyBucket(packageName, userId, elapsedRealtime);
+ }
+ }
+
+ public Map<String, Integer> getAppStandbyBuckets(int userId, long elapsedRealtime) {
+ synchronized (mAppIdleLock) {
+ return mAppIdleHistory.getAppStandbyBuckets(userId, elapsedRealtime, mAppIdleEnabled);
+ }
}
void setAppStandbyBucket(String packageName, int userId, @StandbyBuckets int newBucket,
String reason, long elapsedRealtime) {
- mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
- reason);
+ synchronized (mAppIdleLock) {
+ AppIdleHistory.AppUsageHistory app = mAppIdleHistory.getAppUsageHistory(packageName,
+ userId, elapsedRealtime);
+ boolean predicted = reason != null && reason.startsWith(REASON_PREDICTED);
+ // Don't allow changing bucket if higher than ACTIVE
+ if (app.currentBucket < STANDBY_BUCKET_ACTIVE) return;
+ // Don't allow prediction to change from or to NEVER
+ if ((app.currentBucket == STANDBY_BUCKET_NEVER
+ || newBucket == STANDBY_BUCKET_NEVER)
+ && predicted) {
+ return;
+ }
+ // If the bucket was forced, don't allow prediction to override
+ if (app.bucketingReason.equals(REASON_FORCED) && predicted) return;
+ mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
+ reason);
+ }
maybeInformListeners(packageName, userId, elapsedRealtime,
newBucket);
}
@@ -811,16 +877,20 @@ public class AppStandbyController {
}
void informListeners(String packageName, int userId, int bucket) {
- final boolean idle = bucket >= AppStandby.STANDBY_BUCKET_RARE;
- for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
- listener.onAppIdleStateChanged(packageName, userId, idle, bucket);
+ final boolean idle = bucket >= STANDBY_BUCKET_RARE;
+ synchronized (mPackageAccessListeners) {
+ for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
+ listener.onAppIdleStateChanged(packageName, userId, idle, bucket);
+ }
}
}
void informParoleStateChanged() {
final boolean paroled = isParoledOrCharging();
- for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
- listener.onParoleStateChanged(paroled);
+ synchronized (mPackageAccessListeners) {
+ for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
+ listener.onParoleStateChanged(paroled);
+ }
}
}
@@ -878,6 +948,11 @@ public class AppStandbyController {
String packageName = pi.packageName;
if (pi.applicationInfo != null && pi.applicationInfo.isSystemApp()) {
mAppIdleHistory.reportUsage(packageName, userId, elapsedRealtime);
+ if (isAppSpecial(packageName, UserHandle.getAppId(pi.applicationInfo.uid),
+ userId)) {
+ mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
+ STANDBY_BUCKET_EXEMPTED, REASON_DEFAULT);
+ }
}
}
}
@@ -995,8 +1070,11 @@ public class AppStandbyController {
}
boolean isAppIdleEnabled() {
- return mContext.getResources().getBoolean(
+ final boolean buildFlag = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableAutoPowerModes);
+ final boolean runtimeFlag = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.APP_STANDBY_ENABLED, 1) == 1;
+ return buildFlag && runtimeFlag;
}
boolean isCharging() {
@@ -1067,7 +1145,7 @@ public class AppStandbyController {
break;
case MSG_CHECK_IDLE_STATES:
- if (checkIdleStates(msg.arg1)) {
+ if (checkIdleStates(msg.arg1) && mAppIdleEnabled) {
mHandler.sendMessageDelayed(mHandler.obtainMessage(
MSG_CHECK_IDLE_STATES, msg.arg1, 0),
mCheckIdleIntervalMillis);
@@ -1166,6 +1244,8 @@ public class AppStandbyController {
void registerObserver() {
mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
Settings.Global.APP_IDLE_CONSTANTS), false, this);
+ mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
+ Settings.Global.APP_STANDBY_ENABLED), false, this);
}
@Override
@@ -1175,15 +1255,27 @@ public class AppStandbyController {
}
void updateSettings() {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "appidle=" + Settings.Global.getString(mContext.getContentResolver(),
+ Settings.Global.APP_STANDBY_ENABLED));
+ Slog.d(TAG, "appidleconstants=" + Settings.Global.getString(
+ mContext.getContentResolver(),
+ Settings.Global.APP_IDLE_CONSTANTS));
+ }
+ // Check if app_idle_enabled has changed
+ setAppIdleEnabled(mInjector.isAppIdleEnabled());
+
+ // Look at global settings for this.
+ // TODO: Maybe apply different thresholds for different users.
+ try {
+ mParser.setString(mInjector.getAppIdleSettings());
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Bad value for app idle settings: " + e.getMessage());
+ // fallthrough, mParser is empty and all defaults will be returned.
+ }
+
synchronized (mAppIdleLock) {
- // Look at global settings for this.
- // TODO: Maybe apply different thresholds for different users.
- try {
- mParser.setString(mInjector.getAppIdleSettings());
- } catch (IllegalArgumentException e) {
- Slog.e(TAG, "Bad value for app idle settings: " + e.getMessage());
- // fallthrough, mParser is empty and all defaults will be returned.
- }
// Default: 24 hours between paroles
mAppIdleParoleIntervalMillis = mParser.getLong(KEY_PAROLE_INTERVAL,
diff --git a/com/android/server/usage/StorageStatsService.java b/com/android/server/usage/StorageStatsService.java
index 21b11b05..82f80012 100644
--- a/com/android/server/usage/StorageStatsService.java
+++ b/com/android/server/usage/StorageStatsService.java
@@ -164,6 +164,14 @@ public class StorageStatsService extends IStorageStatsManager.Stub {
}
@Override
+ public boolean isReservedSupported(String volumeUuid, String callingPackage) {
+ enforcePermission(Binder.getCallingUid(), callingPackage);
+
+ // TODO: implement as part of b/62024591
+ return false;
+ }
+
+ @Override
public long getTotalBytes(String volumeUuid, String callingPackage) {
// NOTE: No permissions required
diff --git a/com/android/server/usage/UsageStatsDatabase.java b/com/android/server/usage/UsageStatsDatabase.java
index e55e073c..97054697 100644
--- a/com/android/server/usage/UsageStatsDatabase.java
+++ b/com/android/server/usage/UsageStatsDatabase.java
@@ -503,7 +503,7 @@ class UsageStatsDatabase {
mCal.getTimeInMillis());
mCal.setTimeInMillis(currentTimeMillis);
- mCal.addDays(-7);
+ mCal.addDays(-10);
pruneFilesOlderThan(mIntervalDirs[UsageStatsManager.INTERVAL_DAILY],
mCal.getTimeInMillis());
@@ -803,4 +803,4 @@ class UsageStatsDatabase {
}
directory.delete();
}
-} \ No newline at end of file
+}
diff --git a/com/android/server/usage/UsageStatsService.java b/com/android/server/usage/UsageStatsService.java
index 65c1cefa..cdce4487 100644
--- a/com/android/server/usage/UsageStatsService.java
+++ b/com/android/server/usage/UsageStatsService.java
@@ -20,11 +20,11 @@ import android.Manifest;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.IUidObserver;
-import android.app.usage.AppStandby;
import android.app.usage.ConfigurationStats;
import android.app.usage.IUsageStatsManager;
import android.app.usage.UsageEvents;
-import android.app.usage.AppStandby.StandbyBuckets;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManager.StandbyBuckets;
import android.app.usage.UsageEvents.Event;
import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManagerInternal;
@@ -68,7 +68,10 @@ import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
/**
* A service that collects, aggregates, and persists application usage data.
@@ -149,6 +152,8 @@ public class UsageStatsService extends SystemService implements
publishLocalService(UsageStatsManagerInternal.class, new LocalService());
publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService());
+ // Make sure we initialize the data, in case job scheduler needs it early.
+ getUserDataAndInitializeIfNeededLocked(UserHandle.USER_SYSTEM, mSystemTimeSnapshot);
}
@Override
@@ -490,8 +495,8 @@ public class UsageStatsService extends SystemService implements
flushToDiskLocked();
pw.println("Flushed stats to disk");
return;
- } else {
- // Anything else is a pkg to filter
+ } else if (arg != null && !arg.startsWith("-")) {
+ // Anything else that doesn't start with '-' is a pkg to filter
pkg = arg;
break;
}
@@ -678,18 +683,22 @@ public class UsageStatsService extends SystemService implements
@Override
public int getAppStandbyBucket(String packageName, String callingPackage, int userId) {
- if (!hasPermission(callingPackage)) {
- throw new SecurityException("Don't have permission to query app standby bucket");
- }
-
final int callingUid = Binder.getCallingUid();
try {
userId = ActivityManager.getService().handleIncomingUser(
- Binder.getCallingPid(), callingUid, userId, false, true,
+ Binder.getCallingPid(), callingUid, userId, false, false,
"getAppStandbyBucket", null);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
+ // If the calling app is asking about itself, continue, else check for permission.
+ if (mPackageManagerInternal.getPackageUid(packageName, PackageManager.MATCH_ANY_USER,
+ userId) != callingUid) {
+ if (!hasPermission(callingPackage)) {
+ throw new SecurityException(
+ "Don't have permission to query app standby bucket");
+ }
+ }
final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller(callingUid,
userId);
final long token = Binder.clearCallingIdentity();
@@ -707,6 +716,10 @@ public class UsageStatsService extends SystemService implements
getContext().enforceCallingPermission(Manifest.permission.CHANGE_APP_IDLE_STATE,
"No permission to change app standby state");
+ if (bucket < UsageStatsManager.STANDBY_BUCKET_ACTIVE
+ || bucket > UsageStatsManager.STANDBY_BUCKET_NEVER) {
+ throw new IllegalArgumentException("Cannot set the standby bucket to " + bucket);
+ }
final int callingUid = Binder.getCallingUid();
try {
userId = ActivityManager.getService().handleIncomingUser(
@@ -715,10 +728,18 @@ public class UsageStatsService extends SystemService implements
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
+ final boolean shellCaller = callingUid == 0 || callingUid == Process.SHELL_UID;
+ final String reason = shellCaller
+ ? UsageStatsManager.REASON_FORCED
+ : UsageStatsManager.REASON_PREDICTED + ":" + callingUid;
final long token = Binder.clearCallingIdentity();
try {
- mAppStandby.setAppStandbyBucket(packageName, userId, bucket,
- AppStandby.REASON_PREDICTED + ":" + callingUid,
+ // Caller cannot set their own standby state
+ if (mPackageManagerInternal.getPackageUid(packageName,
+ PackageManager.MATCH_ANY_USER, userId) == callingUid) {
+ throw new IllegalArgumentException("Cannot set your own standby bucket");
+ }
+ mAppStandby.setAppStandbyBucket(packageName, userId, bucket, reason,
SystemClock.elapsedRealtime());
} finally {
Binder.restoreCallingIdentity(token);
@@ -726,6 +747,71 @@ public class UsageStatsService extends SystemService implements
}
@Override
+ public Map getAppStandbyBuckets(String callingPackageName, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ try {
+ userId = ActivityManager.getService().handleIncomingUser(
+ Binder.getCallingPid(), callingUid, userId, false, false,
+ "getAppStandbyBucket", null);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ if (!hasPermission(callingPackageName)) {
+ throw new SecurityException(
+ "Don't have permission to query app standby bucket");
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return mAppStandby.getAppStandbyBuckets(userId,
+ SystemClock.elapsedRealtime());
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void setAppStandbyBuckets(Map appBuckets, int userId) {
+ getContext().enforceCallingPermission(Manifest.permission.CHANGE_APP_IDLE_STATE,
+ "No permission to change app standby state");
+
+ final int callingUid = Binder.getCallingUid();
+ try {
+ userId = ActivityManager.getService().handleIncomingUser(
+ Binder.getCallingPid(), callingUid, userId, false, true,
+ "setAppStandbyBucket", null);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ final boolean shellCaller = callingUid == 0 || callingUid == Process.SHELL_UID;
+ final String reason = shellCaller
+ ? UsageStatsManager.REASON_FORCED
+ : UsageStatsManager.REASON_PREDICTED + ":" + callingUid;
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final long elapsedRealtime = SystemClock.elapsedRealtime();
+ Map<String, Integer> buckets = (Map<String, Integer>) appBuckets;
+ for (Map.Entry<String, Integer> entry: buckets.entrySet()) {
+ String packageName = entry.getKey();
+ int bucket = entry.getValue();
+ if (bucket < UsageStatsManager.STANDBY_BUCKET_ACTIVE
+ || bucket > UsageStatsManager.STANDBY_BUCKET_NEVER) {
+ throw new IllegalArgumentException(
+ "Cannot set the standby bucket to " + bucket);
+ }
+ // Caller cannot set their own standby state
+ if (mPackageManagerInternal.getPackageUid(packageName,
+ PackageManager.MATCH_ANY_USER, userId) == callingUid) {
+ throw new IllegalArgumentException("Cannot set your own standby bucket");
+ }
+ mAppStandby.setAppStandbyBucket(packageName, userId, bucket, reason,
+ elapsedRealtime);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
public void whitelistAppTemporarily(String packageName, long duration, int userId)
throws RemoteException {
StringBuilder reason = new StringBuilder(32);
diff --git a/com/android/server/usb/UsbHostManager.java b/com/android/server/usb/UsbHostManager.java
index dd2e1929..7a352a4d 100644
--- a/com/android/server/usb/UsbHostManager.java
+++ b/com/android/server/usb/UsbHostManager.java
@@ -28,19 +28,23 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.usb.descriptors.UsbDescriptor;
import com.android.server.usb.descriptors.UsbDescriptorParser;
+import com.android.server.usb.descriptors.UsbDeviceDescriptor;
import com.android.server.usb.descriptors.report.TextReportCanvas;
import com.android.server.usb.descriptors.tree.UsbDescriptorsTree;
-import java.util.Collection;
+import java.text.SimpleDateFormat;
+import java.util.Date;
import java.util.HashMap;
+import java.util.LinkedList;
/**
* UsbHostManager manages USB state in host mode.
*/
public class UsbHostManager {
private static final String TAG = UsbHostManager.class.getSimpleName();
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
private final Context mContext;
@@ -63,6 +67,128 @@ public class UsbHostManager {
@GuardedBy("mHandlerLock")
private ComponentName mUsbDeviceConnectionHandler;
+ /*
+ * Member used for tracking connections & disconnections
+ */
+ static final SimpleDateFormat sFormat = new SimpleDateFormat("MM-dd HH:mm:ss:SSS");
+ private static final int MAX_CONNECT_RECORDS = 32;
+ private int mNumConnects; // TOTAL # of connect/disconnect
+ private final LinkedList<ConnectionRecord> mConnections = new LinkedList<ConnectionRecord>();
+ private ConnectionRecord mLastConnect;
+
+ /*
+ * ConnectionRecord
+ * Stores connection/disconnection data.
+ */
+ class ConnectionRecord {
+ long mTimestamp; // Same time-base as system log.
+ String mDeviceAddress;
+
+ static final int CONNECT = 0;
+ static final int CONNECT_BADPARSE = 1;
+ static final int CONNECT_BADDEVICE = 2;
+ static final int DISCONNECT = -1;
+
+ final int mMode;
+ final byte[] mDescriptors;
+
+ ConnectionRecord(String deviceAddress, int mode, byte[] descriptors) {
+ mTimestamp = System.currentTimeMillis();
+ mDeviceAddress = deviceAddress;
+ mMode = mode;
+ mDescriptors = descriptors;
+ }
+
+ private String formatTime() {
+ return (new StringBuilder(sFormat.format(new Date(mTimestamp)))).toString();
+ }
+
+ void dumpShort(IndentingPrintWriter pw) {
+ if (mMode != DISCONNECT) {
+ pw.println(formatTime() + " Connect " + mDeviceAddress + " mode:" + mMode);
+ UsbDescriptorParser parser = new UsbDescriptorParser(mDeviceAddress, mDescriptors);
+
+ UsbDeviceDescriptor deviceDescriptor = parser.getDeviceDescriptor();
+
+ pw.println("manfacturer:0x" + Integer.toHexString(deviceDescriptor.getVendorID())
+ + " product:" + Integer.toHexString(deviceDescriptor.getProductID()));
+ pw.println("isHeadset[in: " + parser.isInputHeadset()
+ + " , out: " + parser.isOutputHeadset() + "]");
+ } else {
+ pw.println(formatTime() + " Disconnect " + mDeviceAddress);
+ }
+ }
+
+ void dumpTree(IndentingPrintWriter pw) {
+ if (mMode != DISCONNECT) {
+ pw.println(formatTime() + " Connect " + mDeviceAddress + " mode:" + mMode);
+ UsbDescriptorParser parser = new UsbDescriptorParser(mDeviceAddress, mDescriptors);
+ StringBuilder stringBuilder = new StringBuilder();
+ UsbDescriptorsTree descriptorTree = new UsbDescriptorsTree();
+ descriptorTree.parse(parser);
+ descriptorTree.report(new TextReportCanvas(parser, stringBuilder));
+
+ stringBuilder.append("isHeadset[in: " + parser.isInputHeadset()
+ + " , out: " + parser.isOutputHeadset() + "]");
+ pw.println(stringBuilder.toString());
+ } else {
+ pw.println(formatTime() + " Disconnect " + mDeviceAddress);
+ }
+ }
+
+ void dumpList(IndentingPrintWriter pw) {
+ if (mMode != DISCONNECT) {
+ pw.println(formatTime() + " Connect " + mDeviceAddress + " mode:" + mMode);
+ UsbDescriptorParser parser = new UsbDescriptorParser(mDeviceAddress, mDescriptors);
+ StringBuilder stringBuilder = new StringBuilder();
+ TextReportCanvas canvas = new TextReportCanvas(parser, stringBuilder);
+ for (UsbDescriptor descriptor : parser.getDescriptors()) {
+ descriptor.report(canvas);
+ }
+ pw.println(stringBuilder.toString());
+
+ pw.println("isHeadset[in: " + parser.isInputHeadset()
+ + " , out: " + parser.isOutputHeadset() + "]");
+ } else {
+ pw.println(formatTime() + " Disconnect " + mDeviceAddress);
+ }
+ }
+
+ private static final int kDumpBytesPerLine = 16;
+
+ void dumpRaw(IndentingPrintWriter pw) {
+ if (mMode != DISCONNECT) {
+ pw.println(formatTime() + " Connect " + mDeviceAddress + " mode:" + mMode);
+ int length = mDescriptors.length;
+ pw.println("Raw Descriptors " + length + " bytes");
+ int dataOffset = 0;
+ for (int line = 0; line < length / kDumpBytesPerLine; line++) {
+ StringBuilder sb = new StringBuilder();
+ for (int offset = 0; offset < kDumpBytesPerLine; offset++) {
+ sb.append("0x")
+ .append(String.format("0x%02X", mDescriptors[dataOffset++]))
+ .append(" ");
+ }
+ pw.println(sb.toString());
+ }
+
+ // remainder
+ StringBuilder sb = new StringBuilder();
+ while (dataOffset < length) {
+ sb.append("0x")
+ .append(String.format("0x%02X", mDescriptors[dataOffset++]))
+ .append(" ");
+ }
+ pw.println(sb.toString());
+ } else {
+ pw.println(formatTime() + " Disconnect " + mDeviceAddress);
+ }
+ }
+ }
+
+ /*
+ * UsbHostManager
+ */
public UsbHostManager(Context context, UsbAlsaManager alsaManager,
UsbSettingsManager settingsManager) {
mContext = context;
@@ -124,6 +250,19 @@ public class UsbHostManager {
}
+ private void addConnectionRecord(String deviceAddress, int mode, byte[] rawDescriptors) {
+ mNumConnects++;
+ while (mConnections.size() >= MAX_CONNECT_RECORDS) {
+ mConnections.removeFirst();
+ }
+ ConnectionRecord rec =
+ new ConnectionRecord(deviceAddress, mode, rawDescriptors);
+ mConnections.add(rec);
+ if (mode != ConnectionRecord.DISCONNECT) {
+ mLastConnect = rec;
+ }
+ }
+
/* Called from JNI in monitorUsbHostBus() to report new USB devices
Returns true if successful, i.e. the USB Audio device descriptors are
correctly parsed and the unique device is added to the audio device list.
@@ -155,27 +294,41 @@ public class UsbHostManager {
if (parser.parseDescriptors(descriptors)) {
UsbDevice newDevice = parser.toAndroidUsbDevice();
- mDevices.put(deviceAddress, newDevice);
-
- // It is fine to call this only for the current user as all broadcasts are sent to
- // all profiles of the user and the dialogs should only show once.
- ComponentName usbDeviceConnectionHandler = getUsbDeviceConnectionHandler();
- if (usbDeviceConnectionHandler == null) {
- getCurrentUserSettings().deviceAttached(newDevice);
+ if (newDevice == null) {
+ Slog.e(TAG, "Couldn't create UsbDevice object.");
+ // Tracking
+ addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT_BADDEVICE,
+ parser.getRawDescriptors());
} else {
- getCurrentUserSettings().deviceAttachedForFixedHandler(newDevice,
- usbDeviceConnectionHandler);
- }
+ mDevices.put(deviceAddress, newDevice);
+
+ // It is fine to call this only for the current user as all broadcasts are
+ // sent to all profiles of the user and the dialogs should only show once.
+ ComponentName usbDeviceConnectionHandler = getUsbDeviceConnectionHandler();
+ if (usbDeviceConnectionHandler == null) {
+ getCurrentUserSettings().deviceAttached(newDevice);
+ } else {
+ getCurrentUserSettings().deviceAttachedForFixedHandler(newDevice,
+ usbDeviceConnectionHandler);
+ }
+
+ // Headset?
+ boolean isInputHeadset = parser.isInputHeadset();
+ boolean isOutputHeadset = parser.isOutputHeadset();
+ Slog.i(TAG, "---- isHeadset[in: " + isInputHeadset
+ + " , out: " + isOutputHeadset + "]");
- // Headset?
- boolean isInputHeadset = parser.isInputHeadset();
- boolean isOutputHeadset = parser.isOutputHeadset();
- Slog.i(TAG, "---- isHeadset[in: " + isInputHeadset
- + " , out: " + isOutputHeadset + "]");
+ mUsbAlsaManager.usbDeviceAdded(newDevice, isInputHeadset, isOutputHeadset);
- mUsbAlsaManager.usbDeviceAdded(newDevice, isInputHeadset, isOutputHeadset);
+ // Tracking
+ addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT,
+ parser.getRawDescriptors());
+ }
} else {
Slog.e(TAG, "Error parsing USB device descriptors for " + deviceAddress);
+ // Tracking
+ addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT_BADPARSE,
+ parser.getRawDescriptors());
return false;
}
}
@@ -196,6 +349,9 @@ public class UsbHostManager {
mUsbAlsaManager.usbDeviceRemoved(device);
mSettingsManager.usbDeviceRemoved(device);
getCurrentUserSettings().usbDeviceRemoved(device);
+
+ // Tracking
+ addConnectionRecord(deviceAddress, ConnectionRecord.DISCONNECT, null);
}
}
}
@@ -237,7 +393,11 @@ public class UsbHostManager {
}
}
- public void dump(IndentingPrintWriter pw) {
+ /**
+ * Dump out various information about the state of USB device connections.
+ *
+ */
+ public void dump(IndentingPrintWriter pw, String[] args) {
pw.println("USB Host State:");
synchronized (mHandlerLock) {
if (mUsbDeviceConnectionHandler != null) {
@@ -249,30 +409,36 @@ public class UsbHostManager {
pw.println(" " + name + ": " + mDevices.get(name));
}
- Collection<UsbDevice> devices = mDevices.values();
- if (devices.size() != 0) {
- pw.println("USB Peripheral Descriptors");
- for (UsbDevice device : devices) {
- StringBuilder stringBuilder = new StringBuilder();
+ // Connections
+ pw.println("" + mNumConnects + " total connects/disconnects");
+ pw.println("Last " + mConnections.size() + " connections/disconnections");
+ for (ConnectionRecord rec : mConnections) {
+ rec.dumpShort(pw);
+ }
- UsbDescriptorParser parser = new UsbDescriptorParser(device.getDeviceName());
- if (parser.parseDevice()) {
- UsbDescriptorsTree descriptorTree = new UsbDescriptorsTree();
- descriptorTree.parse(parser);
+ }
- descriptorTree.report(new TextReportCanvas(parser, stringBuilder));
+ mUsbAlsaManager.dump(pw);
+ }
- stringBuilder.append("isHeadset[in: " + parser.isInputHeadset()
- + " , out: " + parser.isOutputHeadset() + "]");
- } else {
- stringBuilder.append("Error Parsing USB Descriptors");
- }
- pw.println(stringBuilder.toString());
- }
+ /**
+ * Dump various descriptor data.
+ */
+ public void dumpDescriptors(IndentingPrintWriter pw, String[] args) {
+ if (mLastConnect != null) {
+ pw.println("Last Connected USB Device:");
+ if (args.length <= 1 || args[1].equals("-dump-short")) {
+ mLastConnect.dumpShort(pw);
+ } else if (args[1].equals("-dump-tree")) {
+ mLastConnect.dumpTree(pw);
+ } else if (args[1].equals("-dump-list")) {
+ mLastConnect.dumpList(pw);
+ } else if (args[1].equals("-dump-raw")) {
+ mLastConnect.dumpRaw(pw);
}
+ } else {
+ pw.println("No USB Devices have been connected.");
}
-
- mUsbAlsaManager.dump(pw);
}
private native void monitorUsbHostBus();
diff --git a/com/android/server/usb/UsbService.java b/com/android/server/usb/UsbService.java
index 17de83f0..8554cf7b 100644
--- a/com/android/server/usb/UsbService.java
+++ b/com/android/server/usb/UsbService.java
@@ -496,7 +496,7 @@ public class UsbService extends IUsbManager.Stub {
mDeviceManager.dump(pw);
}
if (mHostManager != null) {
- mHostManager.dump(pw);
+ mHostManager.dump(pw, args);
}
if (mPortManager != null) {
mPortManager.dump(pw);
@@ -504,7 +504,7 @@ public class UsbService extends IUsbManager.Stub {
mAlsaManager.dump(pw);
mSettingsManager.dump(pw);
- } else if (args.length == 4 && "set-port-roles".equals(args[0])) {
+ } else if ("set-port-roles".equals(args[0]) && args.length == 4) {
final String portId = args[1];
final int powerRole;
switch (args[2]) {
@@ -546,7 +546,7 @@ public class UsbService extends IUsbManager.Stub {
pw.println();
mPortManager.dump(pw);
}
- } else if (args.length == 3 && "add-port".equals(args[0])) {
+ } else if ("add-port".equals(args[0]) && args.length == 3) {
final String portId = args[1];
final int supportedModes;
switch (args[2]) {
@@ -571,7 +571,7 @@ public class UsbService extends IUsbManager.Stub {
pw.println();
mPortManager.dump(pw);
}
- } else if (args.length == 5 && "connect-port".equals(args[0])) {
+ } else if ("connect-port".equals(args[0]) && args.length == 5) {
final String portId = args[1];
final int mode;
final boolean canChangeMode = args[2].endsWith("?");
@@ -618,30 +618,32 @@ public class UsbService extends IUsbManager.Stub {
pw.println();
mPortManager.dump(pw);
}
- } else if (args.length == 2 && "disconnect-port".equals(args[0])) {
+ } else if ("disconnect-port".equals(args[0]) && args.length == 2) {
final String portId = args[1];
if (mPortManager != null) {
mPortManager.disconnectSimulatedPort(portId, pw);
pw.println();
mPortManager.dump(pw);
}
- } else if (args.length == 2 && "remove-port".equals(args[0])) {
+ } else if ("remove-port".equals(args[0]) && args.length == 2) {
final String portId = args[1];
if (mPortManager != null) {
mPortManager.removeSimulatedPort(portId, pw);
pw.println();
mPortManager.dump(pw);
}
- } else if (args.length == 1 && "reset".equals(args[0])) {
+ } else if ("reset".equals(args[0]) && args.length == 1) {
if (mPortManager != null) {
mPortManager.resetSimulation(pw);
pw.println();
mPortManager.dump(pw);
}
- } else if (args.length == 1 && "ports".equals(args[0])) {
+ } else if ("ports".equals(args[0]) && args.length == 1) {
if (mPortManager != null) {
mPortManager.dump(pw);
}
+ } else if ("dump-descriptors".equals(args[0])) {
+ mHostManager.dumpDescriptors(pw, args);
} else {
pw.println("Dump current USB state or issue command:");
pw.println(" ports");
@@ -678,6 +680,12 @@ public class UsbService extends IUsbManager.Stub {
pw.println(" dumpsys usb add-port \"matrix\" ufp");
pw.println(" dumpsys usb connect-port \"matrix\" ufp sink device");
pw.println(" dumpsys usb reset");
+ pw.println();
+ pw.println("Example USB device descriptors:");
+ pw.println(" dumpsys usb dump-descriptors -dump-short");
+ pw.println(" dumpsys usb dump-descriptors -dump-tree");
+ pw.println(" dumpsys usb dump-descriptors -dump-list");
+ pw.println(" dumpsys usb dump-descriptors -dump-raw");
}
} finally {
Binder.restoreCallingIdentity(ident);
diff --git a/com/android/server/usb/descriptors/UsbConfigDescriptor.java b/com/android/server/usb/descriptors/UsbConfigDescriptor.java
index 993778fa..639aa4e0 100644
--- a/com/android/server/usb/descriptors/UsbConfigDescriptor.java
+++ b/com/android/server/usb/descriptors/UsbConfigDescriptor.java
@@ -17,6 +17,7 @@ package com.android.server.usb.descriptors;
import android.hardware.usb.UsbConfiguration;
import android.hardware.usb.UsbInterface;
+import android.util.Log;
import com.android.server.usb.descriptors.report.ReportCanvas;
@@ -29,6 +30,7 @@ import java.util.ArrayList;
*/
public final class UsbConfigDescriptor extends UsbDescriptor {
private static final String TAG = "UsbConfigDescriptor";
+ private static final boolean DEBUG = false;
private int mTotalLength; // 2:2 Total length in bytes of data returned
private byte mNumInterfaces; // 4:1 Number of Interfaces
@@ -77,10 +79,16 @@ public final class UsbConfigDescriptor extends UsbDescriptor {
}
UsbConfiguration toAndroid(UsbDescriptorParser parser) {
+ if (DEBUG) {
+ Log.d(TAG, " toAndroid()");
+ }
String name = parser.getDescriptorString(mConfigIndex);
UsbConfiguration config = new
UsbConfiguration(mConfigValue, name, mAttribs, mMaxPower);
UsbInterface[] interfaces = new UsbInterface[mInterfaceDescriptors.size()];
+ if (DEBUG) {
+ Log.d(TAG, " " + mInterfaceDescriptors.size() + " interfaces.");
+ }
for (int index = 0; index < mInterfaceDescriptors.size(); index++) {
interfaces[index] = mInterfaceDescriptors.get(index).toAndroid(parser);
}
diff --git a/com/android/server/usb/descriptors/UsbDescriptorParser.java b/com/android/server/usb/descriptors/UsbDescriptorParser.java
index 6c6bd013..7a1e9e2f 100644
--- a/com/android/server/usb/descriptors/UsbDescriptorParser.java
+++ b/com/android/server/usb/descriptors/UsbDescriptorParser.java
@@ -26,13 +26,13 @@ import java.util.ArrayList;
*/
public final class UsbDescriptorParser {
private static final String TAG = "UsbDescriptorParser";
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
private final String mDeviceAddr;
// Descriptor Objects
private static final int DESCRIPTORS_ALLOC_SIZE = 128;
- private ArrayList<UsbDescriptor> mDescriptors = new ArrayList<UsbDescriptor>();
+ private final ArrayList<UsbDescriptor> mDescriptors;
private UsbDeviceDescriptor mDeviceDescriptor;
private UsbConfigDescriptor mCurConfigDescriptor;
@@ -45,6 +45,28 @@ public final class UsbDescriptorParser {
public UsbDescriptorParser(String deviceAddr) {
mDeviceAddr = deviceAddr;
+ mDescriptors = new ArrayList<UsbDescriptor>(DESCRIPTORS_ALLOC_SIZE);
+ }
+
+ /**
+ * Connect this parser to an existing set of already parsed descriptors.
+ * This is useful for reporting.
+ */
+ public UsbDescriptorParser(String deviceAddr, ArrayList<UsbDescriptor> descriptors) {
+ mDeviceAddr = deviceAddr;
+ mDescriptors = descriptors;
+ //TODO some error checking here....
+ mDeviceDescriptor = (UsbDeviceDescriptor) descriptors.get(0);
+ }
+
+ /**
+ * Connect this parser to an byte array containing unparsed (raw) device descriptors
+ * to be parsed (and parse them). Useful for parsing a stored descriptor buffer.
+ */
+ public UsbDescriptorParser(String deviceAddr, byte[] rawDescriptors) {
+ mDeviceAddr = deviceAddr;
+ mDescriptors = new ArrayList<UsbDescriptor>(DESCRIPTORS_ALLOC_SIZE);
+ parseDescriptors(rawDescriptors);
}
public String getDeviceAddr() {
@@ -196,8 +218,6 @@ public final class UsbDescriptorParser {
if (DEBUG) {
Log.d(TAG, "parseDescriptors() - start");
}
- // This will allow us to (probably) alloc mDescriptors just once.
- mDescriptors = new ArrayList<UsbDescriptor>(DESCRIPTORS_ALLOC_SIZE);
ByteStream stream = new ByteStream(descriptors);
while (stream.available() > 0) {
@@ -241,7 +261,7 @@ public final class UsbDescriptorParser {
? parseDescriptors(rawDescriptors) : false;
}
- private byte[] getRawDescriptors() {
+ public byte[] getRawDescriptors() {
return getRawDescriptors_native(mDeviceAddr);
}
@@ -269,10 +289,15 @@ public final class UsbDescriptorParser {
*/
public UsbDevice toAndroidUsbDevice() {
if (mDeviceDescriptor == null) {
+ Log.e(TAG, "toAndroidUsbDevice() ERROR - No Device Descriptor");
return null;
}
- return mDeviceDescriptor.toAndroid(this);
+ UsbDevice device = mDeviceDescriptor.toAndroid(this);
+ if (device == null) {
+ Log.e(TAG, "toAndroidUsbDevice() ERROR Creating Device");
+ }
+ return device;
}
/**
diff --git a/com/android/server/usb/descriptors/UsbDeviceDescriptor.java b/com/android/server/usb/descriptors/UsbDeviceDescriptor.java
index 8e7f0fde..e31e3a31 100644
--- a/com/android/server/usb/descriptors/UsbDeviceDescriptor.java
+++ b/com/android/server/usb/descriptors/UsbDeviceDescriptor.java
@@ -17,6 +17,7 @@ package com.android.server.usb.descriptors;
import android.hardware.usb.UsbConfiguration;
import android.hardware.usb.UsbDevice;
+import android.util.Log;
import com.android.server.usb.descriptors.report.ReportCanvas;
import com.android.server.usb.descriptors.report.UsbStrings;
@@ -30,6 +31,7 @@ import java.util.ArrayList;
*/
public final class UsbDeviceDescriptor extends UsbDescriptor {
private static final String TAG = "UsbDeviceDescriptor";
+ private static final boolean DEBUG = false;
public static final int USBSPEC_1_0 = 0x0100;
public static final int USBSPEC_1_1 = 0x0110;
@@ -113,19 +115,30 @@ public final class UsbDeviceDescriptor extends UsbDescriptor {
* @hide
*/
public UsbDevice toAndroid(UsbDescriptorParser parser) {
+ if (DEBUG) {
+ Log.d(TAG, "toAndroid()");
+ }
+
String mfgName = parser.getDescriptorString(mMfgIndex);
String prodName = parser.getDescriptorString(mProductIndex);
+ if (DEBUG) {
+ Log.d(TAG, " mfgName:" + mfgName + " prodName:" + prodName);
+ }
// Create version string in "%.%" format
String versionString =
Integer.toString(mDeviceRelease >> 8) + "." + (mDeviceRelease & 0xFF);
String serialStr = parser.getDescriptorString(mSerialNum);
+ if (DEBUG) {
+ Log.d(TAG, " versionString:" + versionString + " serialStr:" + serialStr);
+ }
UsbDevice device = new UsbDevice(parser.getDeviceAddr(), mVendorID, mProductID,
mDevClass, mDevSubClass,
mProtocol, mfgName, prodName,
versionString, serialStr);
UsbConfiguration[] configs = new UsbConfiguration[mConfigDescriptors.size()];
+ Log.d(TAG, " " + configs.length + " configs");
for (int index = 0; index < mConfigDescriptors.size(); index++) {
configs[index] = mConfigDescriptors.get(index).toAndroid(parser);
}
diff --git a/com/android/server/usb/descriptors/UsbEndpointDescriptor.java b/com/android/server/usb/descriptors/UsbEndpointDescriptor.java
index 11302380..4da31ea4 100644
--- a/com/android/server/usb/descriptors/UsbEndpointDescriptor.java
+++ b/com/android/server/usb/descriptors/UsbEndpointDescriptor.java
@@ -16,6 +16,7 @@
package com.android.server.usb.descriptors;
import android.hardware.usb.UsbEndpoint;
+import android.util.Log;
import com.android.server.usb.descriptors.report.ReportCanvas;
@@ -26,6 +27,7 @@ import com.android.server.usb.descriptors.report.ReportCanvas;
*/
public class UsbEndpointDescriptor extends UsbDescriptor {
private static final String TAG = "UsbEndpointDescriptor";
+ private static final boolean DEBUG = false;
public static final int MASK_ENDPOINT_ADDRESS = 0b000000000001111;
public static final int MASK_ENDPOINT_DIRECTION = (byte) 0b0000000010000000;
@@ -108,6 +110,12 @@ public class UsbEndpointDescriptor extends UsbDescriptor {
}
/* package */ UsbEndpoint toAndroid(UsbDescriptorParser parser) {
+ if (DEBUG) {
+ Log.d(TAG, "toAndroid() type:"
+ + Integer.toHexString(mAttributes & MASK_ATTRIBS_TRANSTYPE)
+ + " sync:" + Integer.toHexString(mAttributes & MASK_ATTRIBS_SYNCTYPE)
+ + " usage:" + Integer.toHexString(mAttributes & MASK_ATTRIBS_USEAGE));
+ }
return new UsbEndpoint(mEndpointAddress, mAttributes, mPacketSize, mInterval);
}
diff --git a/com/android/server/usb/descriptors/UsbInterfaceDescriptor.java b/com/android/server/usb/descriptors/UsbInterfaceDescriptor.java
index d87b1afb..632e3dc5 100644
--- a/com/android/server/usb/descriptors/UsbInterfaceDescriptor.java
+++ b/com/android/server/usb/descriptors/UsbInterfaceDescriptor.java
@@ -17,6 +17,7 @@ package com.android.server.usb.descriptors;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
+import android.util.Log;
import com.android.server.usb.descriptors.report.ReportCanvas;
import com.android.server.usb.descriptors.report.UsbStrings;
@@ -30,6 +31,7 @@ import java.util.ArrayList;
*/
public class UsbInterfaceDescriptor extends UsbDescriptor {
private static final String TAG = "UsbInterfaceDescriptor";
+ private static final boolean DEBUG = false;
protected int mInterfaceNumber; // 2:1 Number of Interface
protected byte mAlternateSetting; // 3:1 Value used to select alternative setting
@@ -93,6 +95,11 @@ public class UsbInterfaceDescriptor extends UsbDescriptor {
}
UsbInterface toAndroid(UsbDescriptorParser parser) {
+ if (DEBUG) {
+ Log.d(TAG, "toAndroid() class:" + Integer.toHexString(mUsbClass)
+ + " subclass:" + Integer.toHexString(mUsbSubclass)
+ + " " + mEndpointDescriptors.size() + " endpoints.");
+ }
String name = parser.getDescriptorString(mDescrIndex);
UsbInterface ntrface = new UsbInterface(
mInterfaceNumber, mAlternateSetting, name, mUsbClass, mUsbSubclass, mProtocol);
diff --git a/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 2d93da9f..44f55511 100644
--- a/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -67,6 +67,7 @@ import com.android.internal.content.PackageMonitor;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions;
+import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.UiThread;
@@ -75,7 +76,6 @@ import com.android.server.soundtrigger.SoundTriggerInternal;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
-import java.util.TreeSet;
/**
* SystemService that publishes an IVoiceInteractionManagerService.
@@ -390,11 +390,13 @@ public class VoiceInteractionManagerService extends SystemService {
}
public void switchUser(int userHandle) {
- synchronized (this) {
- mCurUser = userHandle;
- mCurUserUnlocked = false;
- switchImplementationIfNeededLocked(false);
- }
+ FgThread.getHandler().post(() -> {
+ synchronized (this) {
+ mCurUser = userHandle;
+ mCurUserUnlocked = false;
+ switchImplementationIfNeededLocked(false);
+ }
+ });
}
void switchImplementationIfNeeded(boolean force) {
@@ -442,11 +444,11 @@ public class VoiceInteractionManagerService extends SystemService {
mImpl.shutdownLocked();
}
if (hasComponent) {
- mImpl = new VoiceInteractionManagerServiceImpl(mContext,
- UiThread.getHandler(), this, mCurUser, serviceComponent);
+ setImplLocked(new VoiceInteractionManagerServiceImpl(mContext,
+ UiThread.getHandler(), this, mCurUser, serviceComponent));
mImpl.startLocked();
} else {
- mImpl = null;
+ setImplLocked(null);
}
}
}
@@ -1177,6 +1179,12 @@ public class VoiceInteractionManagerService extends SystemService {
}
}
+ private void setImplLocked(VoiceInteractionManagerServiceImpl impl) {
+ mImpl = impl;
+ mAmInternal.notifyActiveVoiceInteractionServiceChanged(
+ getActiveServiceComponentName());
+ }
+
class SettingsObserver extends ContentObserver {
SettingsObserver(Handler handler) {
super(handler);
@@ -1219,7 +1227,7 @@ public class VoiceInteractionManagerService extends SystemService {
unloadAllKeyphraseModels();
if (mImpl != null) {
mImpl.shutdownLocked();
- mImpl = null;
+ setImplLocked(null);
}
setCurInteractor(null, userHandle);
setCurRecognizer(null, userHandle);
diff --git a/com/android/server/vr/VrManagerInternal.java b/com/android/server/vr/VrManagerInternal.java
index 7b1e12e2..35b6ad30 100644
--- a/com/android/server/vr/VrManagerInternal.java
+++ b/com/android/server/vr/VrManagerInternal.java
@@ -59,13 +59,6 @@ public abstract class VrManagerInternal {
int userId, int processId, @NonNull ComponentName calling);
/**
- * Set whether the system has acquired a sleep token.
- *
- * @param isAsleep is {@code true} if the device is asleep, or {@code false} otherwise.
- */
- public abstract void onSleepStateChanged(boolean isAsleep);
-
- /**
* Set whether the display used for VR output is on.
*
* @param isScreenOn is {@code true} if the display is on and can receive commands,
@@ -74,13 +67,6 @@ public abstract class VrManagerInternal {
public abstract void onScreenStateChanged(boolean isScreenOn);
/**
- * Set whether the keyguard is currently active/showing.
- *
- * @param isShowing is {@code true} if the keyguard is active/showing.
- */
- public abstract void onKeyguardStateChanged(boolean isShowing);
-
- /**
* Return NO_ERROR if the given package is installed on the device and enabled as a
* VrListenerService for the given current user, or a negative error code indicating a failure.
*
diff --git a/com/android/server/vr/VrManagerService.java b/com/android/server/vr/VrManagerService.java
index 1f4e64e8..de723c67 100644
--- a/com/android/server/vr/VrManagerService.java
+++ b/com/android/server/vr/VrManagerService.java
@@ -19,6 +19,7 @@ import static android.view.Display.INVALID_DISPLAY;
import android.Manifest;
import android.app.ActivityManagerInternal;
+import android.app.ActivityManagerInternal.ScreenObserver;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.INotificationManager;
@@ -58,7 +59,10 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
+
+import com.android.server.FgThread;
import com.android.server.wm.WindowManagerInternal;
+import android.view.inputmethod.InputMethodManagerInternal;
import com.android.internal.R;
import com.android.internal.util.DumpUtils;
@@ -104,7 +108,8 @@ import java.util.Objects;
*
* @hide
*/
-public class VrManagerService extends SystemService implements EnabledComponentChangeListener{
+public class VrManagerService extends SystemService
+ implements EnabledComponentChangeListener, ScreenObserver {
public static final String TAG = "VrManagerService";
static final boolean DBG = false;
@@ -236,15 +241,17 @@ public class VrManagerService extends SystemService implements EnabledComponentC
}
}
- private void setSleepState(boolean isAsleep) {
- setSystemState(FLAG_AWAKE, !isAsleep);
- }
-
private void setScreenOn(boolean isScreenOn) {
setSystemState(FLAG_SCREEN_ON, isScreenOn);
}
- private void setKeyguardShowing(boolean isShowing) {
+ @Override
+ public void onAwakeStateChanged(boolean isAwake) {
+ setSystemState(FLAG_AWAKE, isAwake);
+ }
+
+ @Override
+ public void onKeyguardStateChanged(boolean isShowing) {
setSystemState(FLAG_KEYGUARD_UNLOCKED, !isShowing);
}
@@ -609,6 +616,14 @@ public class VrManagerService extends SystemService implements EnabledComponentC
}
@Override
+ public void setVrInputMethod(ComponentName componentName) {
+ enforceCallerPermissionAnyOf(Manifest.permission.RESTRICTED_VR_ACCESS);
+ InputMethodManagerInternal imm =
+ LocalServices.getService(InputMethodManagerInternal.class);
+ imm.startVrInputMethodNoCheck(componentName);
+ }
+
+ @Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -697,21 +712,11 @@ public class VrManagerService extends SystemService implements EnabledComponentC
}
@Override
- public void onSleepStateChanged(boolean isAsleep) {
- VrManagerService.this.setSleepState(isAsleep);
- }
-
- @Override
public void onScreenStateChanged(boolean isScreenOn) {
VrManagerService.this.setScreenOn(isScreenOn);
}
@Override
- public void onKeyguardStateChanged(boolean isShowing) {
- VrManagerService.this.setKeyguardShowing(isShowing);
- }
-
- @Override
public boolean isCurrentVrListener(String packageName, int userId) {
return VrManagerService.this.isCurrentVrListener(packageName, userId);
}
@@ -764,6 +769,9 @@ public class VrManagerService extends SystemService implements EnabledComponentC
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+ LocalServices.getService(ActivityManagerInternal.class)
+ .registerScreenObserver(this);
+
mNotificationManager = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
synchronized (mLock) {
@@ -819,9 +827,11 @@ public class VrManagerService extends SystemService implements EnabledComponentC
@Override
public void onSwitchUser(int userHandle) {
- synchronized (mLock) {
- mComponentObserver.onUsersChanged();
- }
+ FgThread.getHandler().post(() -> {
+ synchronized (mLock) {
+ mComponentObserver.onUsersChanged();
+ }
+ });
}
diff --git a/com/android/server/wallpaper/IWallpaperManagerService.java b/com/android/server/wallpaper/IWallpaperManagerService.java
new file mode 100644
index 00000000..60b08dd3
--- /dev/null
+++ b/com/android/server/wallpaper/IWallpaperManagerService.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.android.server.wallpaper;
+
+import android.app.IWallpaperManager;
+import android.os.IBinder;
+
+/**
+ * Extended IWallpaperManager which can receive SystemService's lifetime events.
+ */
+interface IWallpaperManagerService extends IWallpaperManager, IBinder {
+ /**
+ * @see com.android.server.SystemService#onBootPhase(int)
+ */
+ void onBootPhase(int phase);
+
+ /**
+ * @see com.android.server.SystemService#onUnlockUser(int)
+ */
+ void onUnlockUser(final int userId);
+} \ No newline at end of file
diff --git a/com/android/server/wallpaper/WallpaperManagerService.java b/com/android/server/wallpaper/WallpaperManagerService.java
index b888ec21..8e916ad3 100644
--- a/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/com/android/server/wallpaper/WallpaperManagerService.java
@@ -99,6 +99,7 @@ import com.android.server.EventLogTags;
import com.android.server.FgThread;
import com.android.server.SystemService;
+import java.lang.reflect.InvocationTargetException;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
@@ -119,14 +120,16 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
+import com.android.internal.R;
-public class WallpaperManagerService extends IWallpaperManager.Stub {
+public class WallpaperManagerService extends IWallpaperManager.Stub
+ implements IWallpaperManagerService {
static final String TAG = "WallpaperManagerService";
static final boolean DEBUG = false;
static final boolean DEBUG_LIVE = DEBUG || true;
public static class Lifecycle extends SystemService {
- private WallpaperManagerService mService;
+ private IWallpaperManagerService mService;
public Lifecycle(Context context) {
super(context);
@@ -134,22 +137,30 @@ public class WallpaperManagerService extends IWallpaperManager.Stub {
@Override
public void onStart() {
- mService = new WallpaperManagerService(getContext());
- publishBinderService(Context.WALLPAPER_SERVICE, mService);
+ try {
+ final Class<? extends IWallpaperManagerService> klass =
+ (Class<? extends IWallpaperManagerService>)Class.forName(
+ getContext().getResources().getString(
+ R.string.config_wallpaperManagerServiceName));
+ mService = klass.getConstructor(Context.class).newInstance(getContext());
+ publishBinderService(Context.WALLPAPER_SERVICE, mService);
+ } catch (Exception exp) {
+ Slog.wtf(TAG, "Failed to instantiate WallpaperManagerService", exp);
+ }
}
@Override
public void onBootPhase(int phase) {
- if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
- mService.systemReady();
- } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
- mService.switchUser(UserHandle.USER_SYSTEM, null);
+ if (mService != null) {
+ mService.onBootPhase(phase);
}
}
@Override
public void onUnlockUser(int userHandle) {
- mService.onUnlockUser(userHandle);
+ if (mService != null) {
+ mService.onUnlockUser(userHandle);
+ }
}
}
@@ -664,6 +675,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub {
final SparseArray<Boolean> mUserRestorecon = new SparseArray<Boolean>();
int mCurrentUserId;
+ boolean mInAmbientMode;
static class WallpaperData {
@@ -942,6 +954,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub {
}
mPaddingChanged = false;
}
+ if (mInfo != null && mInfo.getSupportsAmbientMode()) {
+ try {
+ mEngine.setInAmbientMode(mInAmbientMode);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to set ambient mode state", e);
+ }
+ }
try {
// This will trigger onComputeColors in the wallpaper engine.
// It's fine to be locked in here since the binder is oneway.
@@ -1255,7 +1274,17 @@ public class WallpaperManagerService extends IWallpaperManager.Stub {
mLockWallpaperMap.remove(userId);
}
- void onUnlockUser(final int userId) {
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+ systemReady();
+ } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
+ switchUser(UserHandle.USER_SYSTEM, null);
+ }
+ }
+
+ @Override
+ public void onUnlockUser(final int userId) {
synchronized (mLock) {
if (mCurrentUserId == userId) {
if (mWaitingForUnlock) {
@@ -1722,6 +1751,28 @@ public class WallpaperManagerService extends IWallpaperManager.Stub {
}
}
+ public void setInAmbientMode(boolean inAmbienMode) {
+ final IWallpaperEngine engine;
+ synchronized (mLock) {
+ mInAmbientMode = inAmbienMode;
+ final WallpaperData data = mWallpaperMap.get(mCurrentUserId);
+ if (data != null && data.connection != null && data.connection.mInfo != null
+ && data.connection.mInfo.getSupportsAmbientMode()) {
+ engine = data.connection.mEngine;
+ } else {
+ engine = null;
+ }
+ }
+
+ if (engine != null) {
+ try {
+ engine.setInAmbientMode(inAmbienMode);
+ } catch (RemoteException e) {
+ // Cannot talk to wallpaper engine.
+ }
+ }
+ }
+
@Override
public boolean setLockWallpaperCallback(IWallpaperManagerCallback cb) {
checkPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW);
diff --git a/com/android/server/webkit/SystemImpl.java b/com/android/server/webkit/SystemImpl.java
index 1e334b83..4aa2b372 100644
--- a/com/android/server/webkit/SystemImpl.java
+++ b/com/android/server/webkit/SystemImpl.java
@@ -156,9 +156,10 @@ public class SystemImpl implements SystemInterface {
return mWebViewProviderPackages;
}
- public int getFactoryPackageVersion(String packageName) throws NameNotFoundException {
+ public long getFactoryPackageVersion(String packageName) throws NameNotFoundException {
PackageManager pm = AppGlobals.getInitialApplication().getPackageManager();
- return pm.getPackageInfo(packageName, PackageManager.MATCH_FACTORY_ONLY).versionCode;
+ return pm.getPackageInfo(packageName, PackageManager.MATCH_FACTORY_ONLY)
+ .getLongVersionCode();
}
/**
diff --git a/com/android/server/webkit/SystemInterface.java b/com/android/server/webkit/SystemInterface.java
index d57edcd4..405fe7d4 100644
--- a/com/android/server/webkit/SystemInterface.java
+++ b/com/android/server/webkit/SystemInterface.java
@@ -36,7 +36,7 @@ import java.util.List;
public interface SystemInterface {
public WebViewProviderInfo[] getWebViewPackages();
public int onWebViewProviderChanged(PackageInfo packageInfo);
- public int getFactoryPackageVersion(String packageName) throws NameNotFoundException;
+ public long getFactoryPackageVersion(String packageName) throws NameNotFoundException;
public String getUserChosenWebViewProvider(Context context);
public void updateUserSetting(Context context, String newProviderName);
diff --git a/com/android/server/webkit/WebViewUpdater.java b/com/android/server/webkit/WebViewUpdater.java
index 7fc907f9..7e05e46a 100644
--- a/com/android/server/webkit/WebViewUpdater.java
+++ b/com/android/server/webkit/WebViewUpdater.java
@@ -54,7 +54,7 @@ class WebViewUpdater {
private Context mContext;
private SystemInterface mSystemInterface;
- private int mMinimumVersionCode = -1;
+ private long mMinimumVersionCode = -1;
// Keeps track of the number of running relro creations
private int mNumRelroCreationsStarted = 0;
@@ -430,7 +430,7 @@ class WebViewUpdater {
if (!UserPackage.hasCorrectTargetSdkVersion(packageInfo)) {
return VALIDITY_INCORRECT_SDK_VERSION;
}
- if (!versionCodeGE(packageInfo.versionCode, getMinimumVersionCode())
+ if (!versionCodeGE(packageInfo.getLongVersionCode(), getMinimumVersionCode())
&& !mSystemInterface.systemIsDebuggable()) {
// Webview providers may be downgraded arbitrarily low, prevent that by enforcing
// minimum version code. This check is only enforced for user builds.
@@ -461,9 +461,9 @@ class WebViewUpdater {
*
* @return true if versionCode1 is higher than or equal to versionCode2.
*/
- private static boolean versionCodeGE(int versionCode1, int versionCode2) {
- int v1 = versionCode1 / 100000;
- int v2 = versionCode2 / 100000;
+ private static boolean versionCodeGE(long versionCode1, long versionCode2) {
+ long v1 = versionCode1 / 100000;
+ long v2 = versionCode2 / 100000;
return v1 >= v2;
}
@@ -478,16 +478,16 @@ class WebViewUpdater {
* (mMinimumVersionCode) which is shared between threads. Furthermore, this method does not
* hold mLock meaning that we must take extra care to ensure this method is thread-safe.
*/
- private int getMinimumVersionCode() {
+ private long getMinimumVersionCode() {
if (mMinimumVersionCode > 0) {
return mMinimumVersionCode;
}
- int minimumVersionCode = -1;
+ long minimumVersionCode = -1;
for (WebViewProviderInfo provider : mSystemInterface.getWebViewPackages()) {
if (provider.availableByDefault && !provider.isFallback) {
try {
- int versionCode =
+ long versionCode =
mSystemInterface.getFactoryPackageVersion(provider.packageName);
if (minimumVersionCode < 0 || versionCode < minimumVersionCode) {
minimumVersionCode = versionCode;
@@ -577,7 +577,7 @@ class WebViewUpdater {
String packageDetails = String.format(
"versionName: %s, versionCode: %d, targetSdkVersion: %d",
systemUserPackageInfo.versionName,
- systemUserPackageInfo.versionCode,
+ systemUserPackageInfo.getLongVersionCode(),
systemUserPackageInfo.applicationInfo.targetSdkVersion);
if (validity == VALIDITY_OK) {
boolean installedForAllUsers = isInstalledAndEnabledForAllUsers(
diff --git a/com/android/server/wifi/ExtendedWifiInfo.java b/com/android/server/wifi/ExtendedWifiInfo.java
new file mode 100644
index 00000000..5edbd34e
--- /dev/null
+++ b/com/android/server/wifi/ExtendedWifiInfo.java
@@ -0,0 +1,123 @@
+/*
+ * 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 com.android.server.wifi;
+
+import android.net.wifi.WifiInfo;
+
+/**
+ * Extends WifiInfo with the methods for computing the averaged packet rates
+ */
+public class ExtendedWifiInfo extends WifiInfo {
+ private static final long RESET_TIME_STAMP = Long.MIN_VALUE;
+ private static final double FILTER_TIME_CONSTANT = 3000.0;
+
+ private long mLastPacketCountUpdateTimeStamp = RESET_TIME_STAMP;
+
+ @Override
+ public void reset() {
+ super.reset();
+ mLastPacketCountUpdateTimeStamp = RESET_TIME_STAMP;
+ }
+
+ /**
+ * Updates the packet rates using link layer stats
+ *
+ * @param stats WifiLinkLayerStats
+ * @param timeStamp time in milliseconds
+ */
+ 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) * 1000.0 / timeDelta
+ * currentSampleWeight;
+ txSuccessRate = txSuccessRate * lastSampleWeight
+ + (txgood - txSuccess) * 1000.0 / timeDelta
+ * currentSampleWeight;
+ rxSuccessRate = rxSuccessRate * lastSampleWeight
+ + (rxgood - rxSuccess) * 1000.0 / timeDelta
+ * currentSampleWeight;
+ txRetriesRate = txRetriesRate * lastSampleWeight
+ + (txretries - txRetries) * 1000.0 / 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
+ */
+ 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;
+ }
+
+
+}
diff --git a/com/android/server/wifi/FrameworkFacade.java b/com/android/server/wifi/FrameworkFacade.java
index 2c164448..15b9cc73 100644
--- a/com/android/server/wifi/FrameworkFacade.java
+++ b/com/android/server/wifi/FrameworkFacade.java
@@ -65,6 +65,13 @@ public class FrameworkFacade {
}
/**
+ * Mockable facade to Settings.Secure.getInt(.).
+ */
+ public int getSecureIntegerSetting(Context context, String name, int def) {
+ return Settings.Secure.getInt(context.getContentResolver(), name, def);
+ }
+
+ /**
* Helper method for classes to register a ContentObserver
* {@see ContentResolver#registerContentObserver(Uri,boolean,ContentObserver)}.
*
@@ -79,6 +86,17 @@ public class FrameworkFacade {
contentObserver);
}
+ /**
+ * Helper method for classes to unregister a ContentObserver
+ * {@see ContentResolver#unregisterContentObserver(ContentObserver)}.
+ *
+ * @param context
+ * @param contentObserver
+ */
+ public void unregisterContentObserver(Context context, ContentObserver contentObserver) {
+ context.getContentResolver().unregisterContentObserver(contentObserver);
+ }
+
public IBinder getService(String serviceName) {
return ServiceManager.getService(serviceName);
}
diff --git a/com/android/server/wifi/HalDeviceManager.java b/com/android/server/wifi/HalDeviceManager.java
index ffc7113f..facb0651 100644
--- a/com/android/server/wifi/HalDeviceManager.java
+++ b/com/android/server/wifi/HalDeviceManager.java
@@ -41,6 +41,7 @@ import android.util.Log;
import android.util.LongSparseArray;
import android.util.MutableBoolean;
import android.util.MutableInt;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -60,8 +61,9 @@ import java.util.Set;
* Handles device management through the HAL (HIDL) interface.
*/
public class HalDeviceManager {
- private static final String TAG = "HalDeviceManager";
- private static final boolean DBG = false;
+ private static final String TAG = "HalDevMgr";
+ private static final boolean VDBG = false;
+ private boolean mDbg = false;
private static final int START_HAL_RETRY_INTERVAL_MS = 20;
// Number of attempts a start() is re-tried. A value of 0 means no retries after a single
@@ -77,10 +79,21 @@ public class HalDeviceManager {
public HalDeviceManager(Clock clock) {
mClock = clock;
- mInterfaceAvailableForRequestListeners.put(IfaceType.STA, new HashSet<>());
- mInterfaceAvailableForRequestListeners.put(IfaceType.AP, new HashSet<>());
- mInterfaceAvailableForRequestListeners.put(IfaceType.P2P, new HashSet<>());
- mInterfaceAvailableForRequestListeners.put(IfaceType.NAN, new HashSet<>());
+ mInterfaceAvailableForRequestListeners.put(IfaceType.STA, new HashMap<>());
+ mInterfaceAvailableForRequestListeners.put(IfaceType.AP, new HashMap<>());
+ mInterfaceAvailableForRequestListeners.put(IfaceType.P2P, new HashMap<>());
+ mInterfaceAvailableForRequestListeners.put(IfaceType.NAN, new HashMap<>());
+ }
+
+ /* package */ void enableVerboseLogging(int verbose) {
+ if (verbose > 0) {
+ mDbg = true;
+ } else {
+ mDbg = false;
+ }
+ if (VDBG) {
+ mDbg = true; // just override
+ }
}
/**
@@ -253,10 +266,11 @@ public class HalDeviceManager {
*/
public IWifiChip getChip(IWifiIface iface) {
String name = getName(iface);
- if (DBG) Log.d(TAG, "getChip: iface(name)=" + name);
+ int type = getType(iface);
+ if (VDBG) Log.d(TAG, "getChip: iface(name)=" + name);
synchronized (mLock) {
- InterfaceCacheEntry cacheEntry = mInterfaceInfoCache.get(name);
+ InterfaceCacheEntry cacheEntry = mInterfaceInfoCache.get(Pair.create(name, type));
if (cacheEntry == null) {
Log.e(TAG, "getChip: no entry for iface(name)=" + name);
return null;
@@ -282,10 +296,11 @@ public class HalDeviceManager {
@NonNull InterfaceDestroyedListener destroyedListener,
@Nullable Handler handler) {
String name = getName(iface);
- if (DBG) Log.d(TAG, "registerDestroyedListener: iface(name)=" + name);
+ int type = getType(iface);
+ if (VDBG) Log.d(TAG, "registerDestroyedListener: iface(name)=" + name);
synchronized (mLock) {
- InterfaceCacheEntry cacheEntry = mInterfaceInfoCache.get(name);
+ InterfaceCacheEntry cacheEntry = mInterfaceInfoCache.get(Pair.create(name, type));
if (cacheEntry == null) {
Log.e(TAG, "registerDestroyedListener: no entry for iface(name)=" + name);
return false;
@@ -317,11 +332,23 @@ public class HalDeviceManager {
*/
public void registerInterfaceAvailableForRequestListener(int ifaceType,
@NonNull InterfaceAvailableForRequestListener listener, @Nullable Handler handler) {
- if (DBG) Log.d(TAG, "registerInterfaceAvailableForRequestListener: ifaceType=" + ifaceType);
+ if (VDBG) {
+ Log.d(TAG, "registerInterfaceAvailableForRequestListener: ifaceType=" + ifaceType
+ + ", listener=" + listener + ", handler=" + handler);
+ }
synchronized (mLock) {
- mInterfaceAvailableForRequestListeners.get(ifaceType).add(
- new InterfaceAvailableForRequestListenerProxy(listener, handler));
+ InterfaceAvailableForRequestListenerProxy proxy =
+ new InterfaceAvailableForRequestListenerProxy(listener, handler);
+ if (mInterfaceAvailableForRequestListeners.get(ifaceType).containsKey(proxy)) {
+ if (VDBG) {
+ Log.d(TAG,
+ "registerInterfaceAvailableForRequestListener: dup listener skipped: "
+ + listener);
+ }
+ return;
+ }
+ mInterfaceAvailableForRequestListeners.get(ifaceType).put(proxy, null);
}
WifiChipInfo[] chipInfos = getAllChipInfo();
@@ -340,19 +367,13 @@ public class HalDeviceManager {
public void unregisterInterfaceAvailableForRequestListener(
int ifaceType,
InterfaceAvailableForRequestListener listener) {
- if (DBG) {
+ if (VDBG) {
Log.d(TAG, "unregisterInterfaceAvailableForRequestListener: ifaceType=" + ifaceType);
}
synchronized (mLock) {
- Iterator<InterfaceAvailableForRequestListenerProxy> it =
- mInterfaceAvailableForRequestListeners.get(ifaceType).iterator();
- while (it.hasNext()) {
- if (it.next().mListener == listener) {
- it.remove();
- return;
- }
- }
+ mInterfaceAvailableForRequestListeners.get(ifaceType).remove(
+ new InterfaceAvailableForRequestListenerProxy(listener, null));
}
}
@@ -398,15 +419,15 @@ public class HalDeviceManager {
}
/**
- * Called when an interface type is possibly available for creation.
+ * Called when an interface type availability for creation is changed.
*/
public interface InterfaceAvailableForRequestListener {
/**
- * Registered when an interface type could be requested. Registered with
+ * Called when an interface type availability for creation is updated. Registered with
* registerInterfaceAvailableForRequestListener() and unregistered with
* unregisterInterfaceAvailableForRequestListener().
*/
- void onAvailableForRequest();
+ void onAvailabilityChanged(boolean isAvailable);
}
/**
@@ -415,7 +436,7 @@ public class HalDeviceManager {
* Returns the created IWifiRttController or a null on error.
*/
public IWifiRttController createRttController() {
- if (DBG) Log.d(TAG, "createRttController");
+ if (VDBG) Log.d(TAG, "createRttController");
synchronized (mLock) {
if (mWifi == null) {
Log.e(TAG, "createRttController: null IWifi");
@@ -471,7 +492,7 @@ public class HalDeviceManager {
private IWifi mWifi;
private final WifiEventCallback mWifiEventCallback = new WifiEventCallback();
private final Set<ManagerStatusListenerProxy> mManagerStatusListeners = new HashSet<>();
- private final SparseArray<Set<InterfaceAvailableForRequestListenerProxy>>
+ private final SparseArray<Map<InterfaceAvailableForRequestListenerProxy, Boolean>>
mInterfaceAvailableForRequestListeners = new SparseArray<>();
private final SparseArray<IWifiChipEventCallback.Stub> mDebugCallbacks = new SparseArray<>();
@@ -480,7 +501,8 @@ public class HalDeviceManager {
* we need to keep a list of registered destroyed listeners. Will be validated regularly
* in getAllChipInfoAndValidateCache().
*/
- private final Map<String, InterfaceCacheEntry> mInterfaceInfoCache = new HashMap<>();
+ private final Map<Pair<String, Integer>, InterfaceCacheEntry> mInterfaceInfoCache =
+ new HashMap<>();
private class InterfaceCacheEntry {
public IWifiChip chip;
@@ -595,7 +617,7 @@ public class HalDeviceManager {
* will be to WTF and continue.
*/
private void initIServiceManagerIfNecessary() {
- if (DBG) Log.d(TAG, "initIServiceManagerIfNecessary");
+ if (mDbg) Log.d(TAG, "initIServiceManagerIfNecessary");
synchronized (mLock) {
if (mServiceManager != null) {
@@ -633,7 +655,7 @@ public class HalDeviceManager {
* @return true if supported, false otherwise.
*/
private boolean isSupportedInternal() {
- if (DBG) Log.d(TAG, "isSupportedInternal");
+ if (VDBG) Log.d(TAG, "isSupportedInternal");
synchronized (mLock) {
if (mServiceManager == null) {
@@ -670,7 +692,7 @@ public class HalDeviceManager {
* Here and elsewhere we assume that death listener will do the right thing!
*/
private void initIWifiIfNecessary() {
- if (DBG) Log.d(TAG, "initIWifiIfNecessary");
+ if (mDbg) Log.d(TAG, "initIWifiIfNecessary");
synchronized (mLock) {
if (mWifi != null) {
@@ -712,9 +734,9 @@ public class HalDeviceManager {
* Relies (to the degree we care) on the service removing all listeners when Wi-Fi is stopped.
*/
private void initIWifiChipDebugListeners() {
- if (DBG) Log.d(TAG, "initIWifiChipDebugListeners");
+ if (VDBG) Log.d(TAG, "initIWifiChipDebugListeners");
- if (!DBG) {
+ if (!VDBG) {
return;
}
@@ -736,7 +758,7 @@ public class HalDeviceManager {
return;
}
- if (DBG) Log.d(TAG, "getChipIds=" + chipIdsResp.value);
+ Log.d(TAG, "getChipIds=" + chipIdsResp.value);
if (chipIdsResp.value.size() == 0) {
Log.e(TAG, "Should have at least 1 chip!");
return;
@@ -819,7 +841,7 @@ public class HalDeviceManager {
* reduce the likelihood that we get out-of-sync).
*/
private WifiChipInfo[] getAllChipInfo() {
- if (DBG) Log.d(TAG, "getAllChipInfo");
+ if (VDBG) Log.d(TAG, "getAllChipInfo");
synchronized (mLock) {
if (mWifi == null) {
@@ -844,7 +866,7 @@ public class HalDeviceManager {
return null;
}
- if (DBG) Log.d(TAG, "getChipIds=" + chipIdsResp.value);
+ if (VDBG) Log.d(TAG, "getChipIds=" + chipIdsResp.value);
if (chipIdsResp.value.size() == 0) {
Log.e(TAG, "Should have at least 1 chip!");
return null;
@@ -1064,7 +1086,7 @@ public class HalDeviceManager {
* found on the information read from the chip.
*/
private boolean validateInterfaceCache(WifiChipInfo[] chipInfos) {
- if (DBG) Log.d(TAG, "validateInterfaceCache");
+ if (VDBG) Log.d(TAG, "validateInterfaceCache");
synchronized (mLock) {
for (InterfaceCacheEntry entry: mInterfaceInfoCache.values()) {
@@ -1106,7 +1128,7 @@ public class HalDeviceManager {
}
private boolean isWifiStarted() {
- if (DBG) Log.d(TAG, "isWifiStart");
+ if (VDBG) Log.d(TAG, "isWifiStart");
synchronized (mLock) {
try {
@@ -1124,7 +1146,7 @@ public class HalDeviceManager {
}
private boolean startWifi() {
- if (DBG) Log.d(TAG, "startWifi");
+ if (VDBG) Log.d(TAG, "startWifi");
synchronized (mLock) {
try {
@@ -1170,7 +1192,7 @@ public class HalDeviceManager {
}
private void stopWifi() {
- if (DBG) Log.d(TAG, "stopWifi");
+ if (VDBG) Log.d(TAG, "stopWifi");
synchronized (mLock) {
try {
@@ -1194,13 +1216,13 @@ public class HalDeviceManager {
private class WifiEventCallback extends IWifiEventCallback.Stub {
@Override
public void onStart() throws RemoteException {
- if (DBG) Log.d(TAG, "IWifiEventCallback.onStart");
+ if (VDBG) Log.d(TAG, "IWifiEventCallback.onStart");
// NOP: only happens in reaction to my calls - will handle directly
}
@Override
public void onStop() throws RemoteException {
- if (DBG) Log.d(TAG, "IWifiEventCallback.onStop");
+ if (VDBG) Log.d(TAG, "IWifiEventCallback.onStop");
// NOP: only happens in reaction to my calls - will handle directly
}
@@ -1286,7 +1308,7 @@ public class HalDeviceManager {
private IWifiIface createIface(int ifaceType, InterfaceDestroyedListener destroyedListener,
Handler handler) {
- if (DBG) Log.d(TAG, "createIface: ifaceType=" + ifaceType);
+ if (mDbg) Log.d(TAG, "createIface: ifaceType=" + ifaceType);
synchronized (mLock) {
WifiChipInfo[] chipInfos = getAllChipInfo();
@@ -1316,7 +1338,7 @@ public class HalDeviceManager {
private IWifiIface createIfaceIfPossible(WifiChipInfo[] chipInfos, int ifaceType,
InterfaceDestroyedListener destroyedListener, Handler handler) {
- if (DBG) {
+ if (VDBG) {
Log.d(TAG, "createIfaceIfPossible: chipInfos=" + Arrays.deepToString(chipInfos)
+ ", ifaceType=" + ifaceType);
}
@@ -1327,7 +1349,7 @@ public class HalDeviceManager {
for (IWifiChip.ChipIfaceCombination chipIfaceCombo : chipMode
.availableCombinations) {
int[][] expandedIfaceCombos = expandIfaceCombos(chipIfaceCombo);
- if (DBG) {
+ if (VDBG) {
Log.d(TAG, chipIfaceCombo + " expands to "
+ Arrays.deepToString(expandedIfaceCombos));
}
@@ -1337,7 +1359,7 @@ public class HalDeviceManager {
chipInfo, chipMode, expandedIfaceCombo, ifaceType);
if (compareIfaceCreationData(currentProposal,
bestIfaceCreationProposal)) {
- if (DBG) Log.d(TAG, "new proposal accepted");
+ if (VDBG) Log.d(TAG, "new proposal accepted");
bestIfaceCreationProposal = currentProposal;
}
}
@@ -1361,8 +1383,9 @@ public class HalDeviceManager {
}
cacheEntry.creationTime = mClock.getUptimeSinceBootMillis();
- if (DBG) Log.d(TAG, "createIfaceIfPossible: added cacheEntry=" + cacheEntry);
- mInterfaceInfoCache.put(cacheEntry.name, cacheEntry);
+ if (mDbg) Log.d(TAG, "createIfaceIfPossible: added cacheEntry=" + cacheEntry);
+ mInterfaceInfoCache.put(
+ Pair.create(cacheEntry.name, cacheEntry.type), cacheEntry);
return iface;
}
}
@@ -1374,7 +1397,7 @@ public class HalDeviceManager {
// similar to createIfaceIfPossible - but simpler code: not looking for best option just
// for any option (so terminates on first one).
private boolean isItPossibleToCreateIface(WifiChipInfo[] chipInfos, int ifaceType) {
- if (DBG) {
+ if (VDBG) {
Log.d(TAG, "isItPossibleToCreateIface: chipInfos=" + Arrays.deepToString(chipInfos)
+ ", ifaceType=" + ifaceType);
}
@@ -1384,7 +1407,7 @@ public class HalDeviceManager {
for (IWifiChip.ChipIfaceCombination chipIfaceCombo : chipMode
.availableCombinations) {
int[][] expandedIfaceCombos = expandIfaceCombos(chipIfaceCombo);
- if (DBG) {
+ if (VDBG) {
Log.d(TAG, chipIfaceCombo + " expands to "
+ Arrays.deepToString(expandedIfaceCombos));
}
@@ -1465,14 +1488,14 @@ public class HalDeviceManager {
*/
private IfaceCreationData canIfaceComboSupportRequest(WifiChipInfo chipInfo,
IWifiChip.ChipMode chipMode, int[] chipIfaceCombo, int ifaceType) {
- if (DBG) {
+ if (VDBG) {
Log.d(TAG, "canIfaceComboSupportRequest: chipInfo=" + chipInfo + ", chipMode="
+ chipMode + ", chipIfaceCombo=" + chipIfaceCombo + ", ifaceType=" + ifaceType);
}
// short-circuit: does the chipIfaceCombo even support the requested type?
if (chipIfaceCombo[ifaceType] == 0) {
- if (DBG) Log.d(TAG, "Requested type not supported by combo");
+ if (VDBG) Log.d(TAG, "Requested type not supported by combo");
return null;
}
@@ -1485,8 +1508,8 @@ public class HalDeviceManager {
for (int type: IFACE_TYPES_BY_PRIORITY) {
if (chipInfo.ifaces[type].length != 0) {
if (!allowedToDeleteIfaceTypeForRequestedType(type, ifaceType,
- chipInfo.ifaces[ifaceType].length != 0)) {
- if (DBG) {
+ chipInfo.ifaces)) {
+ if (VDBG) {
Log.d(TAG, "Couldn't delete existing type " + type
+ " interfaces for requested type");
}
@@ -1515,9 +1538,8 @@ public class HalDeviceManager {
}
if (tooManyInterfaces > 0) { // may need to delete some
- if (!allowedToDeleteIfaceTypeForRequestedType(type, ifaceType,
- chipInfo.ifaces[ifaceType].length != 0)) {
- if (DBG) {
+ if (!allowedToDeleteIfaceTypeForRequestedType(type, ifaceType, chipInfo.ifaces)) {
+ if (VDBG) {
Log.d(TAG, "Would need to delete some higher priority interfaces");
}
return null;
@@ -1547,7 +1569,7 @@ public class HalDeviceManager {
* - Proposal is better if it means removing fewer high priority interfaces
*/
private boolean compareIfaceCreationData(IfaceCreationData val1, IfaceCreationData val2) {
- if (DBG) Log.d(TAG, "compareIfaceCreationData: val1=" + val1 + ", val2=" + val2);
+ if (VDBG) Log.d(TAG, "compareIfaceCreationData: val1=" + val1 + ", val2=" + val2);
// deal with trivial case of one or the other being null
if (val1 == null) {
@@ -1575,7 +1597,7 @@ public class HalDeviceManager {
}
if (numIfacesToDelete1 < numIfacesToDelete2) {
- if (DBG) {
+ if (VDBG) {
Log.d(TAG, "decision based on type=" + type + ": " + numIfacesToDelete1
+ " < " + numIfacesToDelete2);
}
@@ -1584,7 +1606,7 @@ public class HalDeviceManager {
}
// arbitrary - flip a coin
- if (DBG) Log.d(TAG, "proposals identical - flip a coin");
+ if (VDBG) Log.d(TAG, "proposals identical - flip a coin");
return false;
}
@@ -1592,37 +1614,48 @@ public class HalDeviceManager {
* Returns true if we're allowed to delete the existing interface type for the requested
* interface type.
*
- * Rules:
- * 1. Request for AP or STA will destroy any other interface (except see #4 and #5)
- * 2. Request for P2P will destroy NAN-only
- * 3. Request for NAN will not destroy any interface
- * --
- * 4. No interface will be destroyed for a requested interface of the same type
- * 5. No interface will be destroyed if one of the requested interfaces already exists
+ * Rules - applies in order:
+ *
+ * General rules:
+ * 1. No interface will be destroyed for a requested interface of the same type
+ * 2. No interface will be destroyed if one of the requested interfaces already exists
+ * 3. If there are >1 interface of an existing type, then it is ok to destroy that type
+ * interface
+ *
+ * Type-specific rules (but note that the general rules are appied first):
+ * 4. Request for AP or STA will destroy any other interface
+ * 5. Request for P2P will destroy NAN-only (but will destroy a second STA per #3)
+ * 6. Request for NAN will not destroy any interface (but will destroy a second STA per #3)
+
*/
private boolean allowedToDeleteIfaceTypeForRequestedType(int existingIfaceType,
- int requestedIfaceType, boolean requestedIfaceTypeAlreadyExists) {
- // rule 5
- if (requestedIfaceTypeAlreadyExists) {
+ int requestedIfaceType, WifiIfaceInfo[][] currentIfaces) {
+ // rule 1
+ if (existingIfaceType == requestedIfaceType) {
return false;
}
- // rule 4
- if (existingIfaceType == requestedIfaceType) {
+ // rule 2
+ if (currentIfaces[requestedIfaceType].length != 0) {
return false;
}
// rule 3
+ if (currentIfaces[existingIfaceType].length > 1) {
+ return true;
+ }
+
+ // rule 6
if (requestedIfaceType == IfaceType.NAN) {
return false;
}
- // rule 2
+ // rule 5
if (requestedIfaceType == IfaceType.P2P) {
return existingIfaceType == IfaceType.NAN;
}
- // rule 1, the requestIfaceType is either AP or STA
+ // rule 4, the requestIfaceType is either AP or STA
return true;
}
@@ -1636,7 +1669,7 @@ public class HalDeviceManager {
*/
private List<WifiIfaceInfo> selectInterfacesToDelete(int excessInterfaces,
WifiIfaceInfo[] interfaces) {
- if (DBG) {
+ if (VDBG) {
Log.d(TAG, "selectInterfacesToDelete: excessInterfaces=" + excessInterfaces
+ ", interfaces=" + Arrays.toString(interfaces));
}
@@ -1644,7 +1677,8 @@ public class HalDeviceManager {
boolean lookupError = false;
LongSparseArray<WifiIfaceInfo> orderedList = new LongSparseArray(interfaces.length);
for (WifiIfaceInfo info : interfaces) {
- InterfaceCacheEntry cacheEntry = mInterfaceInfoCache.get(info.name);
+ InterfaceCacheEntry cacheEntry = mInterfaceInfoCache.get(
+ Pair.create(info.name, getType(info.iface)));
if (cacheEntry == null) {
Log.e(TAG,
"selectInterfacesToDelete: can't find cache entry with name=" + info.name);
@@ -1676,7 +1710,7 @@ public class HalDeviceManager {
*/
private IWifiIface executeChipReconfiguration(IfaceCreationData ifaceCreationData,
int ifaceType) {
- if (DBG) {
+ if (mDbg) {
Log.d(TAG, "executeChipReconfiguration: ifaceCreationData=" + ifaceCreationData
+ ", ifaceType=" + ifaceType);
}
@@ -1685,7 +1719,7 @@ public class HalDeviceManager {
// is this a mode change?
boolean isModeConfigNeeded = !ifaceCreationData.chipInfo.currentModeIdValid
|| ifaceCreationData.chipInfo.currentModeId != ifaceCreationData.chipModeId;
- if (DBG) Log.d(TAG, "isModeConfigNeeded=" + isModeConfigNeeded);
+ if (mDbg) Log.d(TAG, "isModeConfigNeeded=" + isModeConfigNeeded);
// first delete interfaces/change modes
if (isModeConfigNeeded) {
@@ -1763,7 +1797,13 @@ public class HalDeviceManager {
private boolean removeIfaceInternal(IWifiIface iface) {
String name = getName(iface);
- if (DBG) Log.d(TAG, "removeIfaceInternal: iface(name)=" + name);
+ int type = getType(iface);
+ if (mDbg) Log.d(TAG, "removeIfaceInternal: iface(name)=" + name + ", type=" + type);
+
+ if (type == -1) {
+ Log.e(TAG, "removeIfaceInternal: can't get type -- iface(name)=" + name);
+ return false;
+ }
synchronized (mLock) {
if (mWifi == null) {
@@ -1782,12 +1822,6 @@ public class HalDeviceManager {
return false;
}
- int type = getType(iface);
- if (type == -1) {
- Log.e(TAG, "removeIfaceInternal: can't get type -- iface(name)=" + name);
- return false;
- }
-
WifiStatus status = null;
try {
switch (type) {
@@ -1812,7 +1846,7 @@ public class HalDeviceManager {
}
// dispatch listeners no matter what status
- dispatchDestroyedListeners(name);
+ dispatchDestroyedListeners(name, type);
if (status != null && status.code == WifiStatusCode.SUCCESS) {
return true;
@@ -1826,7 +1860,7 @@ public class HalDeviceManager {
// dispatch all available for request listeners of the specified type AND clean-out the list:
// listeners are called once at most!
private boolean dispatchAvailableForRequestListeners() {
- if (DBG) Log.d(TAG, "dispatchAvailableForRequestListeners");
+ if (VDBG) Log.d(TAG, "dispatchAvailableForRequestListeners");
synchronized (mLock) {
WifiChipInfo[] chipInfos = getAllChipInfo();
@@ -1835,7 +1869,7 @@ public class HalDeviceManager {
stopWifi(); // major error: shutting down
return false;
}
- if (DBG) {
+ if (VDBG) {
Log.d(TAG, "dispatchAvailableForRequestListeners: chipInfos="
+ Arrays.deepToString(chipInfos));
}
@@ -1850,33 +1884,42 @@ public class HalDeviceManager {
private void dispatchAvailableForRequestListenersForType(int ifaceType,
WifiChipInfo[] chipInfos) {
- if (DBG) Log.d(TAG, "dispatchAvailableForRequestListenersForType: ifaceType=" + ifaceType);
+ if (VDBG) Log.d(TAG, "dispatchAvailableForRequestListenersForType: ifaceType=" + ifaceType);
- Set<InterfaceAvailableForRequestListenerProxy> listeners =
- mInterfaceAvailableForRequestListeners.get(ifaceType);
+ synchronized (mLock) {
+ Map<InterfaceAvailableForRequestListenerProxy, Boolean> listeners =
+ mInterfaceAvailableForRequestListeners.get(ifaceType);
- if (listeners.size() == 0) {
- return;
- }
+ if (listeners.size() == 0) {
+ return;
+ }
- if (!isItPossibleToCreateIface(chipInfos, ifaceType)) {
- if (DBG) Log.d(TAG, "Creating interface type isn't possible: ifaceType=" + ifaceType);
- return;
- }
+ boolean isAvailable = isItPossibleToCreateIface(chipInfos, ifaceType);
- if (DBG) Log.d(TAG, "It is possible to create the interface type: ifaceType=" + ifaceType);
- for (InterfaceAvailableForRequestListenerProxy listener : listeners) {
- listener.trigger();
+ if (VDBG) {
+ Log.d(TAG, "Interface available for: ifaceType=" + ifaceType + " = " + isAvailable);
+ }
+ for (Map.Entry<InterfaceAvailableForRequestListenerProxy, Boolean> listenerEntry :
+ listeners.entrySet()) {
+ if (listenerEntry.getValue() == null || listenerEntry.getValue() != isAvailable) {
+ if (VDBG) {
+ Log.d(TAG, "Interface available listener dispatched: ifaceType=" + ifaceType
+ + ", listener=" + listenerEntry.getKey());
+ }
+ listenerEntry.getKey().triggerWithArg(isAvailable);
+ }
+ listenerEntry.setValue(isAvailable);
+ }
}
}
// dispatch all destroyed listeners registered for the specified interface AND remove the
// cache entry
- private void dispatchDestroyedListeners(String name) {
- if (DBG) Log.d(TAG, "dispatchDestroyedListeners: iface(name)=" + name);
+ private void dispatchDestroyedListeners(String name, int type) {
+ if (VDBG) Log.d(TAG, "dispatchDestroyedListeners: iface(name)=" + name);
synchronized (mLock) {
- InterfaceCacheEntry entry = mInterfaceInfoCache.get(name);
+ InterfaceCacheEntry entry = mInterfaceInfoCache.get(Pair.create(name, type));
if (entry == null) {
Log.e(TAG, "dispatchDestroyedListeners: no cache entry for iface(name)=" + name);
return;
@@ -1886,16 +1929,16 @@ public class HalDeviceManager {
listener.trigger();
}
entry.destroyedListeners.clear(); // for insurance (though cache entry is removed)
- mInterfaceInfoCache.remove(name);
+ mInterfaceInfoCache.remove(Pair.create(name, type));
}
}
// dispatch all destroyed listeners registered to all interfaces
private void dispatchAllDestroyedListeners() {
- if (DBG) Log.d(TAG, "dispatchAllDestroyedListeners");
+ if (VDBG) Log.d(TAG, "dispatchAllDestroyedListeners");
synchronized (mLock) {
- Iterator<Map.Entry<String, InterfaceCacheEntry>> it =
+ Iterator<Map.Entry<Pair<String, Integer>, InterfaceCacheEntry>> it =
mInterfaceInfoCache.entrySet().iterator();
while (it.hasNext()) {
InterfaceCacheEntry entry = it.next().getValue();
@@ -1934,7 +1977,18 @@ public class HalDeviceManager {
}
}
- protected abstract void action();
+ void triggerWithArg(boolean arg) {
+ if (mHandler != null) {
+ mHandler.post(() -> {
+ actionWithArg(arg);
+ });
+ } else {
+ actionWithArg(arg);
+ }
+ }
+
+ protected void action() {}
+ protected void actionWithArg(boolean arg) {}
ListenerProxy(LISTENER listener, Handler handler, String tag) {
mListener = listener;
@@ -1966,8 +2020,8 @@ public class HalDeviceManager {
}
@Override
- protected void action() {
- mListener.onAvailableForRequest();
+ protected void actionWithArg(boolean isAvailable) {
+ mListener.onAvailabilityChanged(isAvailable);
}
}
diff --git a/com/android/server/wifi/LegacyConnectedScore.java b/com/android/server/wifi/LegacyConnectedScore.java
index facab0a5..30276960 100644
--- a/com/android/server/wifi/LegacyConnectedScore.java
+++ b/com/android/server/wifi/LegacyConnectedScore.java
@@ -63,6 +63,10 @@ public class LegacyConnectedScore extends ConnectedScore {
private boolean mMultiBandScanResults;
private boolean mIsHomeNetwork;
private int mScore = 0;
+ private int mBadRssiCount;
+ private int mLinkStuckCount;
+ private int mLowRssiCount;
+
LegacyConnectedScore(Context context, WifiConfigManager wifiConfigManager, Clock clock) {
super(clock);
@@ -109,32 +113,32 @@ public class LegacyConnectedScore extends ConnectedScore {
rssi += WifiConfiguration.HOME_NETWORK_RSSI_BOOST;
}
- if ((wifiInfo.txBadRate >= 1)
- && (wifiInfo.txSuccessRate < MAX_SUCCESS_RATE_OF_STUCK_LINK)
+ if ((wifiInfo.txBadRate * 5 >= 1)
+ && (wifiInfo.txSuccessRate * 5 < MAX_SUCCESS_RATE_OF_STUCK_LINK)
&& rssi < rssiThreshLow) {
// Link is stuck
- if (wifiInfo.linkStuckCount < MAX_STUCK_LINK_COUNT) {
- wifiInfo.linkStuckCount += 1;
+ if (mLinkStuckCount < MAX_STUCK_LINK_COUNT) {
+ mLinkStuckCount += 1;
}
- } else if (wifiInfo.txBadRate < MIN_TX_FAILURE_RATE_FOR_WORKING_LINK) {
- if (wifiInfo.linkStuckCount > 0) {
- wifiInfo.linkStuckCount -= 1;
+ } else if (wifiInfo.txBadRate * 5 < MIN_TX_FAILURE_RATE_FOR_WORKING_LINK) {
+ if (mLinkStuckCount > 0) {
+ mLinkStuckCount -= 1;
}
}
if (rssi < rssiThreshBad) {
- if (wifiInfo.badRssiCount < MAX_BAD_RSSI_COUNT) {
- wifiInfo.badRssiCount += 1;
+ if (mBadRssiCount < MAX_BAD_RSSI_COUNT) {
+ mBadRssiCount += 1;
}
} else if (rssi < rssiThreshLow) {
- wifiInfo.lowRssiCount = MAX_LOW_RSSI_COUNT; // Dont increment the lowRssi count above 1
- if (wifiInfo.badRssiCount > 0) {
+ mLowRssiCount = MAX_LOW_RSSI_COUNT; // Dont increment the lowRssi count above 1
+ if (mBadRssiCount > 0) {
// Decrement bad Rssi count
- wifiInfo.badRssiCount -= 1;
+ mBadRssiCount -= 1;
}
} else {
- wifiInfo.badRssiCount = 0;
- wifiInfo.lowRssiCount = 0;
+ mBadRssiCount = 0;
+ mLowRssiCount = 0;
}
// Ugh, we need to finish the score calculation while we have wifiInfo
@@ -155,6 +159,9 @@ public class LegacyConnectedScore extends ConnectedScore {
@Override
public void reset() {
mScore = 0;
+ mBadRssiCount = 0;
+ mLinkStuckCount = 0;
+ mLowRssiCount = 0;
}
/**
@@ -182,18 +189,19 @@ public class LegacyConnectedScore extends ConnectedScore {
int linkSpeed = wifiInfo.getLinkSpeed();
- if (wifiInfo.linkStuckCount > MIN_SUSTAINED_LINK_STUCK_COUNT) {
+ if (mLinkStuckCount > MIN_SUSTAINED_LINK_STUCK_COUNT) {
// Once link gets stuck for more than 3 seconds, start reducing the score
- score = score - LINK_STUCK_PENALTY * (wifiInfo.linkStuckCount - 1);
+ score = score - LINK_STUCK_PENALTY * (mLinkStuckCount - 1);
}
if (linkSpeed < linkspeedThreshBad) {
score -= BAD_LINKSPEED_PENALTY;
- } else if ((linkSpeed >= linkspeedThreshGood) && (wifiInfo.txSuccessRate > 5)) {
+ } else if ((linkSpeed >= linkspeedThreshGood)
+ && (wifiInfo.txSuccessRate > 1)) {
score += GOOD_LINKSPEED_BONUS; // So as bad rssi alone doesn't kill us
}
- score -= wifiInfo.badRssiCount * BAD_RSSI_COUNT_PENALTY + wifiInfo.lowRssiCount;
+ score -= mBadRssiCount * BAD_RSSI_COUNT_PENALTY + mLowRssiCount;
if (rssi >= rssiThreshSaturated) score += 5;
diff --git a/com/android/server/wifi/SavedNetworkEvaluator.java b/com/android/server/wifi/SavedNetworkEvaluator.java
index 59d4ab26..97f6b6f0 100644
--- a/com/android/server/wifi/SavedNetworkEvaluator.java
+++ b/com/android/server/wifi/SavedNetworkEvaluator.java
@@ -25,8 +25,6 @@ import android.util.Pair;
import com.android.internal.R;
import com.android.server.wifi.util.TelephonyUtil;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
@@ -242,96 +240,83 @@ public class SavedNetworkEvaluator implements WifiNetworkSelector.NetworkEvaluat
for (ScanDetail scanDetail : scanDetails) {
ScanResult scanResult = scanDetail.getScanResult();
- int highestScoreOfScanResult = Integer.MIN_VALUE;
- int candidateIdOfScanResult = WifiConfiguration.INVALID_NETWORK_ID;
// One ScanResult can be associated with more than one networks, hence we calculate all
// the scores and use the highest one as the ScanResult's score.
- List<WifiConfiguration> associatedConfigurations = null;
- WifiConfiguration associatedConfiguration =
+ WifiConfiguration network =
mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail);
- if (associatedConfiguration == null) {
+ if (network == null) {
continue;
- } else {
- associatedConfigurations =
- new ArrayList<>(Arrays.asList(associatedConfiguration));
}
- for (WifiConfiguration network : associatedConfigurations) {
- /**
- * Ignore Passpoint and Ephemeral networks. They are configured networks,
- * but without being persisted to the storage. They are evaluated by
- * {@link PasspointNetworkEvaluator} and {@link ScoredNetworkEvaluator}
- * respectively.
- */
- if (network.isPasspoint() || network.isEphemeral()) {
- continue;
- }
+ /**
+ * Ignore Passpoint and Ephemeral networks. They are configured networks,
+ * but without being persisted to the storage. They are evaluated by
+ * {@link PasspointNetworkEvaluator} and {@link ScoredNetworkEvaluator}
+ * respectively.
+ */
+ if (network.isPasspoint() || network.isEphemeral()) {
+ continue;
+ }
- WifiConfiguration.NetworkSelectionStatus status =
- network.getNetworkSelectionStatus();
- status.setSeenInLastQualifiedNetworkSelection(true);
-
- if (!status.isNetworkEnabled()) {
- continue;
- } else if (network.BSSID != null && !network.BSSID.equals("any")
- && !network.BSSID.equals(scanResult.BSSID)) {
- // App has specified the only BSSID to connect for this
- // configuration. So only the matching ScanResult can be a candidate.
- localLog("Network " + WifiNetworkSelector.toNetworkString(network)
- + " has specified BSSID " + network.BSSID + ". Skip "
- + scanResult.BSSID);
- continue;
- } else if (TelephonyUtil.isSimConfig(network)
- && !mWifiConfigManager.isSimPresent()) {
- // Don't select if security type is EAP SIM/AKA/AKA' when SIM is not present.
- continue;
- }
+ WifiConfiguration.NetworkSelectionStatus status =
+ network.getNetworkSelectionStatus();
+ status.setSeenInLastQualifiedNetworkSelection(true);
- int score = calculateBssidScore(scanResult, network, currentNetwork, currentBssid,
- scoreHistory);
-
- // Set candidate ScanResult for all saved networks to ensure that users can
- // override network selection. See WifiNetworkSelector#setUserConnectChoice.
- // TODO(b/36067705): consider alternative designs to push filtering/selecting of
- // user connect choice networks to RecommendedNetworkEvaluator.
- if (score > status.getCandidateScore() || (score == status.getCandidateScore()
- && status.getCandidate() != null
- && scanResult.level > status.getCandidate().level)) {
- mWifiConfigManager.setNetworkCandidateScanResult(
- network.networkId, scanResult, score);
- }
+ if (!status.isNetworkEnabled()) {
+ continue;
+ } else if (network.BSSID != null && !network.BSSID.equals("any")
+ && !network.BSSID.equals(scanResult.BSSID)) {
+ // App has specified the only BSSID to connect for this
+ // configuration. So only the matching ScanResult can be a candidate.
+ localLog("Network " + WifiNetworkSelector.toNetworkString(network)
+ + " has specified BSSID " + network.BSSID + ". Skip "
+ + scanResult.BSSID);
+ continue;
+ } else if (TelephonyUtil.isSimConfig(network)
+ && !mWifiConfigManager.isSimPresent()) {
+ // Don't select if security type is EAP SIM/AKA/AKA' when SIM is not present.
+ continue;
+ }
- // If the network is marked to use external scores, or is an open network with
- // curate saved open networks enabled, do not consider it for network selection.
- if (network.useExternalScores) {
- localLog("Network " + WifiNetworkSelector.toNetworkString(network)
- + " has external score.");
- continue;
- }
+ int score = calculateBssidScore(scanResult, network, currentNetwork, currentBssid,
+ scoreHistory);
- if (score > highestScoreOfScanResult) {
- highestScoreOfScanResult = score;
- candidateIdOfScanResult = network.networkId;
- }
+ // Set candidate ScanResult for all saved networks to ensure that users can
+ // override network selection. See WifiNetworkSelector#setUserConnectChoice.
+ // TODO(b/36067705): consider alternative designs to push filtering/selecting of
+ // user connect choice networks to RecommendedNetworkEvaluator.
+ if (score > status.getCandidateScore() || (score == status.getCandidateScore()
+ && status.getCandidate() != null
+ && scanResult.level > status.getCandidate().level)) {
+ mWifiConfigManager.setNetworkCandidateScanResult(
+ network.networkId, scanResult, score);
+ }
+
+ // If the network is marked to use external scores, or is an open network with
+ // curate saved open networks enabled, do not consider it for network selection.
+ if (network.useExternalScores) {
+ localLog("Network " + WifiNetworkSelector.toNetworkString(network)
+ + " has external score.");
+ continue;
}
if (connectableNetworks != null) {
connectableNetworks.add(Pair.create(scanDetail,
- mWifiConfigManager.getConfiguredNetwork(candidateIdOfScanResult)));
+ mWifiConfigManager.getConfiguredNetwork(network.networkId)));
}
- if (highestScoreOfScanResult > highestScore
- || (highestScoreOfScanResult == highestScore
+ if (score > highestScore
+ || (score == highestScore
&& scanResultCandidate != null
&& scanResult.level > scanResultCandidate.level)) {
- highestScore = highestScoreOfScanResult;
+ highestScore = score;
scanResultCandidate = scanResult;
mWifiConfigManager.setNetworkCandidateScanResult(
- candidateIdOfScanResult, scanResultCandidate, highestScore);
+ network.networkId, scanResultCandidate, highestScore);
// Reload the network config with the updated info.
- candidate = mWifiConfigManager.getConfiguredNetwork(candidateIdOfScanResult);
+ candidate = mWifiConfigManager.getConfiguredNetwork(network.networkId);
}
}
diff --git a/com/android/server/wifi/SoftApManager.java b/com/android/server/wifi/SoftApManager.java
index cec9887c..6d5fd278 100644
--- a/com/android/server/wifi/SoftApManager.java
+++ b/com/android/server/wifi/SoftApManager.java
@@ -23,21 +23,28 @@ import static com.android.server.wifi.util.ApConfigUtil.SUCCESS;
import android.annotation.NonNull;
import android.content.Context;
import android.content.Intent;
+import android.database.ContentObserver;
import android.net.InterfaceConfiguration;
import android.net.wifi.IApInterface;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
+import android.os.Handler;
import android.os.INetworkManagementService;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.UserHandle;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
+import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
+import com.android.internal.util.WakeupMessage;
import com.android.server.net.BaseNetworkObserver;
import com.android.server.wifi.WifiNative.SoftApListener;
import com.android.server.wifi.util.ApConfigUtil;
@@ -51,8 +58,15 @@ import java.util.Locale;
public class SoftApManager implements ActiveModeManager {
private static final String TAG = "SoftApManager";
- private final Context mContext;
+ // Minimum limit to use for timeout delay if the value from overlay setting is too small.
+ private static final int MIN_SOFT_AP_TIMEOUT_DELAY_MS = 600_000; // 10 minutes
+
+ @VisibleForTesting
+ public static final String SOFT_AP_SEND_MESSAGE_TIMEOUT_TAG = TAG
+ + " Soft AP Send Message Timeout";
+ private final Context mContext;
+ private final FrameworkFacade mFrameworkFacade;
private final WifiNative mWifiNative;
private final String mCountryCode;
@@ -61,8 +75,8 @@ public class SoftApManager implements ActiveModeManager {
private final Listener mListener;
- private final IApInterface mApInterface;
- private final String mApInterfaceName;
+ private IApInterface mApInterface;
+ private String mApInterfaceName;
private final INetworkManagementService mNwService;
private final WifiApConfigStore mWifiApConfigStore;
@@ -72,8 +86,9 @@ public class SoftApManager implements ActiveModeManager {
private final int mMode;
private WifiConfiguration mApConfig;
- private int mNumAssociatedStations = 0;
-
+ /**
+ * Listener for soft AP events.
+ */
private final SoftApListener mSoftApListener = new SoftApListener() {
@Override
public void onNumAssociatedStationsChanged(int numStations) {
@@ -82,7 +97,6 @@ public class SoftApManager implements ActiveModeManager {
}
};
-
/**
* Listener for soft AP state changes.
*/
@@ -97,23 +111,19 @@ public class SoftApManager implements ActiveModeManager {
public SoftApManager(Context context,
Looper looper,
+ FrameworkFacade framework,
WifiNative wifiNative,
String countryCode,
Listener listener,
- @NonNull IApInterface apInterface,
- @NonNull String ifaceName,
INetworkManagementService nms,
WifiApConfigStore wifiApConfigStore,
@NonNull SoftApModeConfiguration apConfig,
WifiMetrics wifiMetrics) {
- mStateMachine = new SoftApStateMachine(looper);
-
mContext = context;
+ mFrameworkFacade = framework;
mWifiNative = wifiNative;
mCountryCode = countryCode;
mListener = listener;
- mApInterface = apInterface;
- mApInterfaceName = ifaceName;
mNwService = nms;
mWifiApConfigStore = wifiApConfigStore;
mMode = apConfig.getTargetMode();
@@ -124,6 +134,7 @@ public class SoftApManager implements ActiveModeManager {
mApConfig = config;
}
mWifiMetrics = wifiMetrics;
+ mStateMachine = new SoftApStateMachine(looper);
}
/**
@@ -141,29 +152,6 @@ public class SoftApManager implements ActiveModeManager {
}
/**
- * Get number of stations associated with this soft AP
- */
- @VisibleForTesting
- public int getNumAssociatedStations() {
- return mNumAssociatedStations;
- }
-
- /**
- * Set number of stations associated with this soft AP
- * @param numStations Number of connected stations
- */
- private void setNumAssociatedStations(int numStations) {
- if (mNumAssociatedStations == numStations) {
- return;
- }
- mNumAssociatedStations = numStations;
- Log.d(TAG, "Number of associated stations changed: " + mNumAssociatedStations);
-
- // TODO:(b/63906412) send it up to settings.
- mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(mNumAssociatedStations, mMode);
- }
-
- /**
* Update AP state.
* @param newState new AP state
* @param currentState current AP state
@@ -190,6 +178,17 @@ public class SoftApManager implements ActiveModeManager {
}
/**
+ * Helper function to increment the appropriate setup failure metrics.
+ */
+ private void incrementMetricsForSetupFailure(int failureReason) {
+ if (failureReason == WifiNative.SETUP_FAILURE_HAL) {
+ mWifiMetrics.incrementNumWifiOnFailureDueToHal();
+ } else if (failureReason == WifiNative.SETUP_FAILURE_WIFICOND) {
+ mWifiMetrics.incrementNumWifiOnFailureDueToWificond();
+ }
+ }
+
+ /**
* Start a soft AP instance with the given configuration.
* @param config AP configuration
* @return integer result code
@@ -236,12 +235,11 @@ public class SoftApManager implements ActiveModeManager {
}
/**
- * Teardown soft AP.
+ * Teardown soft AP and teardown the interface.
*/
private void stopSoftAp() {
if (!mWifiNative.stopSoftAp()) {
- Log.d(TAG, "Soft AP stop failed");
- return;
+ Log.e(TAG, "Soft AP stop failed");
}
Log.d(TAG, "Soft AP is stopped");
}
@@ -250,15 +248,18 @@ public class SoftApManager implements ActiveModeManager {
// Commands for the state machine.
public static final int CMD_START = 0;
public static final int CMD_STOP = 1;
- public static final int CMD_AP_INTERFACE_BINDER_DEATH = 2;
+ public static final int CMD_WIFICOND_BINDER_DEATH = 2;
public static final int CMD_INTERFACE_STATUS_CHANGED = 3;
public static final int CMD_NUM_ASSOCIATED_STATIONS_CHANGED = 4;
+ public static final int CMD_NO_ASSOCIATED_STATIONS_TIMEOUT = 5;
+ public static final int CMD_TIMEOUT_TOGGLE_CHANGED = 6;
private final State mIdleState = new IdleState();
private final State mStartedState = new StartedState();
- private final StateMachineDeathRecipient mDeathRecipient =
- new StateMachineDeathRecipient(this, CMD_AP_INTERFACE_BINDER_DEATH);
+ private final WifiNative.WificondDeathEventHandler mWificondDeathRecipient = () -> {
+ sendMessage(CMD_WIFICOND_BINDER_DEATH);
+ };
private NetworkObserver mNetworkObserver;
@@ -291,7 +292,7 @@ public class SoftApManager implements ActiveModeManager {
private class IdleState extends State {
@Override
public void enter() {
- mDeathRecipient.unlinkToDeath();
+ mWifiNative.deregisterWificondDeathHandler();
unregisterObserver();
}
@@ -299,6 +300,39 @@ public class SoftApManager implements ActiveModeManager {
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_START:
+ // need to create our interface
+ mApInterface = null;
+ Pair<Integer, IApInterface> statusAndInterface =
+ mWifiNative.setupForSoftApMode(mWifiNative.getInterfaceName());
+ if (statusAndInterface.first == WifiNative.SETUP_SUCCESS) {
+ mApInterface = statusAndInterface.second;
+ } else {
+ Log.e(TAG, "setup failure when creating ap interface.");
+ incrementMetricsForSetupFailure(statusAndInterface.first);
+ }
+ if (mApInterface == null) {
+ Log.e(TAG, "Not starting softap mode without an interface.");
+ updateApState(WifiManager.WIFI_AP_STATE_FAILED,
+ WifiManager.WIFI_AP_STATE_DISABLED,
+ WifiManager.SAP_START_FAILURE_GENERAL);
+ mWifiMetrics.incrementSoftApStartResult(
+ false, WifiManager.SAP_START_FAILURE_GENERAL);
+ break;
+ }
+ try {
+ mApInterfaceName = mApInterface.getInterfaceName();
+ } catch (RemoteException e) {
+ // Failed to get the interface name. This is not a good sign and we
+ // should report a failure.
+ Log.e(TAG, "Failed to get the interface name.");
+ updateApState(WifiManager.WIFI_AP_STATE_FAILED,
+ WifiManager.WIFI_AP_STATE_DISABLED,
+ WifiManager.SAP_START_FAILURE_GENERAL);
+ mWifiMetrics.incrementSoftApStartResult(
+ false, WifiManager.SAP_START_FAILURE_GENERAL);
+ break;
+ }
+
// first a sanity check on the interface name. If we failed to retrieve it,
// we are going to have a hard time setting up routing.
if (TextUtils.isEmpty(mApInterfaceName)) {
@@ -312,9 +346,7 @@ public class SoftApManager implements ActiveModeManager {
}
updateApState(WifiManager.WIFI_AP_STATE_ENABLING,
WifiManager.WIFI_AP_STATE_DISABLED, 0);
- setNumAssociatedStations(0);
- if (!mDeathRecipient.linkToDeath(mApInterface.asBinder())) {
- mDeathRecipient.unlinkToDeath();
+ if (!mWifiNative.registerWificondDeathHandler(mWificondDeathRecipient)) {
updateApState(WifiManager.WIFI_AP_STATE_FAILED,
WifiManager.WIFI_AP_STATE_ENABLING,
WifiManager.SAP_START_FAILURE_GENERAL);
@@ -322,12 +354,11 @@ public class SoftApManager implements ActiveModeManager {
false, WifiManager.SAP_START_FAILURE_GENERAL);
break;
}
-
try {
mNetworkObserver = new NetworkObserver(mApInterfaceName);
mNwService.registerObserver(mNetworkObserver);
} catch (RemoteException e) {
- mDeathRecipient.unlinkToDeath();
+ mWifiNative.deregisterWificondDeathHandler();
unregisterObserver();
updateApState(WifiManager.WIFI_AP_STATE_FAILED,
WifiManager.WIFI_AP_STATE_ENABLING,
@@ -343,7 +374,7 @@ public class SoftApManager implements ActiveModeManager {
if (result == ERROR_NO_CHANNEL) {
failureReason = WifiManager.SAP_START_FAILURE_NO_CHANNEL;
}
- mDeathRecipient.unlinkToDeath();
+ mWifiNative.deregisterWificondDeathHandler();
unregisterObserver();
updateApState(WifiManager.WIFI_AP_STATE_FAILED,
WifiManager.WIFI_AP_STATE_ENABLING,
@@ -376,6 +407,92 @@ public class SoftApManager implements ActiveModeManager {
private class StartedState extends State {
private boolean mIfaceIsUp;
+ private int mNumAssociatedStations;
+
+ private boolean mTimeoutEnabled;
+ private int mTimeoutDelay;
+ private WakeupMessage mSoftApTimeoutMessage;
+ private SoftApTimeoutEnabledSettingObserver mSettingObserver;
+
+ /**
+ * Observer for timeout settings changes.
+ */
+ private class SoftApTimeoutEnabledSettingObserver extends ContentObserver {
+ SoftApTimeoutEnabledSettingObserver(Handler handler) {
+ super(handler);
+ }
+
+ public void register() {
+ mFrameworkFacade.registerContentObserver(mContext,
+ Settings.Global.getUriFor(Settings.Global.SOFT_AP_TIMEOUT_ENABLED),
+ true, this);
+ mTimeoutEnabled = getValue();
+ }
+
+ public void unregister() {
+ mFrameworkFacade.unregisterContentObserver(mContext, this);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ mStateMachine.sendMessage(SoftApStateMachine.CMD_TIMEOUT_TOGGLE_CHANGED,
+ getValue() ? 1 : 0);
+ }
+
+ private boolean getValue() {
+ boolean enabled = mFrameworkFacade.getIntegerSetting(mContext,
+ Settings.Global.SOFT_AP_TIMEOUT_ENABLED, 1) == 1;
+ return enabled;
+ }
+ }
+
+ private int getConfigSoftApTimeoutDelay() {
+ int delay = mContext.getResources().getInteger(
+ R.integer.config_wifi_framework_soft_ap_timeout_delay);
+ if (delay < MIN_SOFT_AP_TIMEOUT_DELAY_MS) {
+ delay = MIN_SOFT_AP_TIMEOUT_DELAY_MS;
+ Log.w(TAG, "Overriding timeout delay with minimum limit value");
+ }
+ Log.d(TAG, "Timeout delay: " + delay);
+ return delay;
+ }
+
+ private void scheduleTimeoutMessage() {
+ if (!mTimeoutEnabled) {
+ return;
+ }
+ mSoftApTimeoutMessage.schedule(SystemClock.elapsedRealtime() + mTimeoutDelay);
+ Log.d(TAG, "Timeout message scheduled");
+ }
+
+ private void cancelTimeoutMessage() {
+ mSoftApTimeoutMessage.cancel();
+ Log.d(TAG, "Timeout message canceled");
+ }
+
+ /**
+ * Set number of stations associated with this soft AP
+ * @param numStations Number of connected stations
+ */
+ private void setNumAssociatedStations(int numStations) {
+ if (mNumAssociatedStations == numStations) {
+ return;
+ }
+ mNumAssociatedStations = numStations;
+ Log.d(TAG, "Number of associated stations changed: " + mNumAssociatedStations);
+
+ // TODO:(b/63906412) send it up to settings.
+ mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(mNumAssociatedStations,
+ mMode);
+
+ if (mNumAssociatedStations == 0) {
+ scheduleTimeoutMessage();
+ } else {
+ cancelTimeoutMessage();
+ }
+ }
+
private void onUpChanged(boolean isUp) {
if (isUp == mIfaceIsUp) {
return; // no change
@@ -389,9 +506,7 @@ public class SoftApManager implements ActiveModeManager {
} else {
// TODO: handle the case where the interface was up, but goes down
}
-
mWifiMetrics.addSoftApUpChangedEvent(isUp, mMode);
- setNumAssociatedStations(0);
}
@Override
@@ -405,6 +520,30 @@ public class SoftApManager implements ActiveModeManager {
if (config != null) {
onUpChanged(config.isUp());
}
+
+ mTimeoutDelay = getConfigSoftApTimeoutDelay();
+ Handler handler = mStateMachine.getHandler();
+ mSoftApTimeoutMessage = new WakeupMessage(mContext, handler,
+ SOFT_AP_SEND_MESSAGE_TIMEOUT_TAG,
+ SoftApStateMachine.CMD_NO_ASSOCIATED_STATIONS_TIMEOUT);
+ mSettingObserver = new SoftApTimeoutEnabledSettingObserver(handler);
+
+ if (mSettingObserver != null) {
+ mSettingObserver.register();
+ }
+ Log.d(TAG, "Resetting num stations on start");
+ mNumAssociatedStations = 0;
+ scheduleTimeoutMessage();
+ }
+
+ @Override
+ public void exit() {
+ if (mSettingObserver != null) {
+ mSettingObserver.unregister();
+ }
+ Log.d(TAG, "Resetting num stations on stop");
+ mNumAssociatedStations = 0;
+ cancelTimeoutMessage();
}
@Override
@@ -415,8 +554,22 @@ public class SoftApManager implements ActiveModeManager {
Log.e(TAG, "Invalid number of associated stations: " + message.arg1);
break;
}
+ Log.d(TAG, "Setting num stations on CMD_NUM_ASSOCIATED_STATIONS_CHANGED");
setNumAssociatedStations(message.arg1);
break;
+ case CMD_TIMEOUT_TOGGLE_CHANGED:
+ boolean isEnabled = (message.arg1 == 1);
+ if (mTimeoutEnabled == isEnabled) {
+ break;
+ }
+ mTimeoutEnabled = isEnabled;
+ if (!mTimeoutEnabled) {
+ cancelTimeoutMessage();
+ }
+ if (mTimeoutEnabled && mNumAssociatedStations == 0) {
+ scheduleTimeoutMessage();
+ }
+ break;
case CMD_INTERFACE_STATUS_CHANGED:
if (message.obj != mNetworkObserver) {
// This is from some time before the most recent configuration.
@@ -428,13 +581,23 @@ public class SoftApManager implements ActiveModeManager {
case CMD_START:
// Already started, ignore this command.
break;
- case CMD_AP_INTERFACE_BINDER_DEATH:
+ case CMD_NO_ASSOCIATED_STATIONS_TIMEOUT:
+ if (!mTimeoutEnabled) {
+ Log.wtf(TAG, "Timeout message received while timeout is disabled."
+ + " Dropping.");
+ break;
+ }
+ if (mNumAssociatedStations != 0) {
+ Log.wtf(TAG, "Timeout message received but has clients. Dropping.");
+ break;
+ }
+ Log.i(TAG, "Timeout message received. Stopping soft AP.");
+ case CMD_WIFICOND_BINDER_DEATH:
case CMD_STOP:
updateApState(WifiManager.WIFI_AP_STATE_DISABLING,
WifiManager.WIFI_AP_STATE_ENABLED, 0);
- setNumAssociatedStations(0);
stopSoftAp();
- if (message.what == CMD_AP_INTERFACE_BINDER_DEATH) {
+ if (message.what == CMD_WIFICOND_BINDER_DEATH) {
updateApState(WifiManager.WIFI_AP_STATE_FAILED,
WifiManager.WIFI_AP_STATE_DISABLING,
WifiManager.SAP_START_FAILURE_GENERAL);
diff --git a/com/android/server/wifi/StateMachineDeathRecipient.java b/com/android/server/wifi/StateMachineDeathRecipient.java
deleted file mode 100644
index 64c4beec..00000000
--- a/com/android/server/wifi/StateMachineDeathRecipient.java
+++ /dev/null
@@ -1,86 +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 com.android.server.wifi;
-
-import android.os.IBinder;
-import android.os.IBinder.DeathRecipient;
-import android.os.RemoteException;
-import com.android.internal.util.StateMachine;
-
-/**
- * Allows StateMachine instances to subscribe to binder death.
- *
- * @hide
- */
-public class StateMachineDeathRecipient implements DeathRecipient {
-
- private final StateMachine mStateMachine;
- private final int mDeathCommand;
- private IBinder mLinkedBinder;
-
- /**
- * Construct a StateMachineDeathRecipient.
- *
- * @param sm StateMachine instance to receive a message upon Binder death.
- * @param command message to send the state machine.
- */
- public StateMachineDeathRecipient(StateMachine sm, int command) {
- mStateMachine = sm;
- mDeathCommand = command;
- }
-
- /**
- * Listen for the death of a binder.
- *
- * This method will unlink from death notifications from any
- * previously linked IBinder instance.
- *
- * @param binder remote object to listen for death.
- * @return true iff we have successfully subscribed to death notifications of a live
- * IBinder instance.
- */
- public boolean linkToDeath(IBinder binder) {
- unlinkToDeath();
- try {
- binder.linkToDeath(this, 0);
- } catch (RemoteException e) {
- // The remote has already died.
- return false;
- }
- mLinkedBinder = binder;
- return true;
- }
-
- /**
- * Unlink from notifications from the last linked IBinder instance.
- */
- public void unlinkToDeath() {
- if (mLinkedBinder == null) {
- return;
- }
- mLinkedBinder.unlinkToDeath(this, 0);
- mLinkedBinder = null;
- }
-
- /**
- * Called by the binder subsystem upon remote object death.
- */
- @Override
- public void binderDied() {
- mStateMachine.sendMessage(mDeathCommand);
- }
-} \ No newline at end of file
diff --git a/com/android/server/wifi/SupplicantStaIfaceHal.java b/com/android/server/wifi/SupplicantStaIfaceHal.java
index c93e1102..3429e3d5 100644
--- a/com/android/server/wifi/SupplicantStaIfaceHal.java
+++ b/com/android/server/wifi/SupplicantStaIfaceHal.java
@@ -53,6 +53,7 @@ import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
+import com.android.server.wifi.WifiNative.SupplicantDeathEventHandler;
import com.android.server.wifi.hotspot2.AnqpEvent;
import com.android.server.wifi.hotspot2.IconEvent;
import com.android.server.wifi.hotspot2.WnmData;
@@ -97,8 +98,15 @@ public class SupplicantStaIfaceHal {
// Supplicant HAL interface objects
private IServiceManager mIServiceManager = null;
private ISupplicant mISupplicant;
- private ISupplicantStaIface mISupplicantStaIface;
- private ISupplicantStaIfaceCallback mISupplicantStaIfaceCallback;
+ private HashMap<String, ISupplicantStaIface> mISupplicantStaIfaces = new HashMap<>();
+ private HashMap<String, ISupplicantStaIfaceCallback> mISupplicantStaIfaceCallbacks =
+ new HashMap<>();
+ private HashMap<String, SupplicantStaNetworkHal> mCurrentNetworkRemoteHandles = new HashMap<>();
+ private HashMap<String, WifiConfiguration> mCurrentNetworkLocalConfigs = new HashMap<>();
+ private SupplicantDeathEventHandler mDeathEventHandler;
+ private final Context mContext;
+ private final WifiMonitor mWifiMonitor;
+
private final IServiceNotification mServiceNotificationCallback =
new IServiceNotification.Stub() {
public void onRegistration(String fqName, String name, boolean preexisting) {
@@ -107,11 +115,11 @@ public class SupplicantStaIfaceHal {
Log.i(TAG, "IServiceNotification.onRegistration for: " + fqName
+ ", " + name + " preexisting=" + preexisting);
}
- if (!initSupplicantService() || !initSupplicantStaIface()) {
- Log.e(TAG, "initalizing ISupplicantIfaces failed.");
+ if (!initSupplicantService()) {
+ Log.e(TAG, "initalizing ISupplicant failed.");
supplicantServiceDiedHandler();
} else {
- Log.i(TAG, "Completed initialization of ISupplicant interfaces.");
+ Log.i(TAG, "Completed initialization of ISupplicant.");
}
}
}
@@ -132,16 +140,10 @@ public class SupplicantStaIfaceHal {
}
};
- private String mIfaceName;
- private SupplicantStaNetworkHal mCurrentNetworkRemoteHandle;
- private WifiConfiguration mCurrentNetworkLocalConfig;
- private final Context mContext;
- private final WifiMonitor mWifiMonitor;
public SupplicantStaIfaceHal(Context context, WifiMonitor monitor) {
mContext = context;
mWifiMonitor = monitor;
- mISupplicantStaIfaceCallback = new SupplicantStaIfaceHalCallback();
}
/**
@@ -184,7 +186,7 @@ public class SupplicantStaIfaceHal {
Log.i(TAG, "Registering ISupplicant service ready callback.");
}
mISupplicant = null;
- mISupplicantStaIface = null;
+ mISupplicantStaIfaces.clear();
if (mIServiceManager != null) {
// Already have an IServiceManager and serviceNotification registered, don't
// don't register another.
@@ -253,11 +255,11 @@ public class SupplicantStaIfaceHal {
return true;
}
- private boolean linkToSupplicantStaIfaceDeath() {
+ private boolean linkToSupplicantStaIfaceDeath(ISupplicantStaIface iface) {
synchronized (mLock) {
- if (mISupplicantStaIface == null) return false;
+ if (iface == null) return false;
try {
- if (!mISupplicantStaIface.linkToDeath(mSupplicantDeathRecipient, 0)) {
+ if (!iface.linkToDeath(mSupplicantDeathRecipient, 0)) {
Log.wtf(TAG, "Error on linkToDeath on ISupplicantStaIface");
supplicantServiceDiedHandler();
return false;
@@ -270,22 +272,59 @@ public class SupplicantStaIfaceHal {
}
}
- private int getCurrentNetworkId() {
+ private int getCurrentNetworkId(@NonNull String ifaceName) {
synchronized (mLock) {
- if (mCurrentNetworkLocalConfig == null) {
+ WifiConfiguration currentConfig = getCurrentNetworkLocalConfig(ifaceName);
+ if (currentConfig == null) {
return WifiConfiguration.INVALID_NETWORK_ID;
}
- return mCurrentNetworkLocalConfig.networkId;
+ return currentConfig.networkId;
}
}
- private boolean initSupplicantStaIface() {
+ /**
+ * Setup a STA interface for the specified iface name.
+ *
+ * @param ifaceName Name of the interface.
+ * @return true on success, false otherwise.
+ */
+ public boolean setupIface(@NonNull String ifaceName) {
+ ISupplicantIface ifaceHwBinder;
+ if (isV1_1()) {
+ ifaceHwBinder = addIfaceV1_1(ifaceName);
+ } else {
+ ifaceHwBinder = getIfaceV1_0(ifaceName);
+ }
+ if (ifaceHwBinder == null) {
+ Log.e(TAG, "setupIface got null iface");
+ return false;
+ }
+ ISupplicantStaIface iface = getStaIfaceMockable(ifaceHwBinder);
+ if (!linkToSupplicantStaIfaceDeath(iface)) {
+ return false;
+ }
+ ISupplicantStaIfaceCallback callback = new SupplicantStaIfaceHalCallback(ifaceName);
+ if (!registerCallback(iface, callback)) {
+ return false;
+ }
+ mISupplicantStaIfaces.put(ifaceName, iface);
+ mISupplicantStaIfaceCallbacks.put(ifaceName, callback);
+ return true;
+ }
+
+ /**
+ * Get a STA interface for the specified iface name.
+ *
+ * @param ifaceName Name of the interface.
+ * @return true on success, false otherwise.
+ */
+ private ISupplicantIface getIfaceV1_0(@NonNull String ifaceName) {
synchronized (mLock) {
/** List all supplicant Ifaces */
final ArrayList<ISupplicant.IfaceInfo> supplicantIfaces = new ArrayList<>();
try {
mISupplicant.listInterfaces((SupplicantStatus status,
- ArrayList<ISupplicant.IfaceInfo> ifaces) -> {
+ ArrayList<ISupplicant.IfaceInfo> ifaces) -> {
if (status.code != SupplicantStatusCode.SUCCESS) {
Log.e(TAG, "Getting Supplicant Interfaces failed: " + status.code);
return;
@@ -294,54 +333,169 @@ public class SupplicantStaIfaceHal {
});
} catch (RemoteException e) {
Log.e(TAG, "ISupplicant.listInterfaces exception: " + e);
- return false;
+ supplicantServiceDiedHandler(ifaceName);
+ return null;
}
if (supplicantIfaces.size() == 0) {
Log.e(TAG, "Got zero HIDL supplicant ifaces. Stopping supplicant HIDL startup.");
- return false;
+ return null;
}
Mutable<ISupplicantIface> supplicantIface = new Mutable<>();
- Mutable<String> ifaceName = new Mutable<>();
for (ISupplicant.IfaceInfo ifaceInfo : supplicantIfaces) {
- if (ifaceInfo.type == IfaceType.STA) {
+ if (ifaceInfo.type == IfaceType.STA && ifaceName.equals(ifaceInfo.name)) {
try {
mISupplicant.getInterface(ifaceInfo,
(SupplicantStatus status, ISupplicantIface iface) -> {
- if (status.code != SupplicantStatusCode.SUCCESS) {
- Log.e(TAG, "Failed to get ISupplicantIface " + status.code);
- return;
- }
- supplicantIface.value = iface;
- });
+ if (status.code != SupplicantStatusCode.SUCCESS) {
+ Log.e(TAG, "Failed to get ISupplicantIface " + status.code);
+ return;
+ }
+ supplicantIface.value = iface;
+ });
} catch (RemoteException e) {
Log.e(TAG, "ISupplicant.getInterface exception: " + e);
- return false;
+ supplicantServiceDiedHandler(ifaceName);
+ return null;
}
- ifaceName.value = ifaceInfo.name;
break;
}
}
- if (supplicantIface.value == null) {
- Log.e(TAG, "initSupplicantStaIface got null iface");
- return false;
+ return supplicantIface.value;
+ }
+ }
+
+ /**
+ * Create a STA interface for the specified iface name.
+ *
+ * @param ifaceName Name of the interface.
+ * @return true on success, false otherwise.
+ */
+ private ISupplicantIface addIfaceV1_1(@NonNull String ifaceName) {
+ synchronized (mLock) {
+ ISupplicant.IfaceInfo ifaceInfo = new ISupplicant.IfaceInfo();
+ ifaceInfo.name = ifaceName;
+ ifaceInfo.type = IfaceType.STA;
+ Mutable<ISupplicantIface> supplicantIface = new Mutable<>();
+ try {
+ getSupplicantMockableV1_1().addInterface(ifaceInfo,
+ (SupplicantStatus status, ISupplicantIface iface) -> {
+ if (status.code != SupplicantStatusCode.SUCCESS
+ && status.code != SupplicantStatusCode.FAILURE_IFACE_EXISTS) {
+ Log.e(TAG, "Failed to create ISupplicantIface " + status.code);
+ return;
+ }
+ supplicantIface.value = iface;
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "ISupplicant.createInterface exception: " + e);
+ supplicantServiceDiedHandler(ifaceName);
+ return null;
}
- mISupplicantStaIface = getStaIfaceMockable(supplicantIface.value);
- mIfaceName = ifaceName.value;
- if (!linkToSupplicantStaIfaceDeath()) {
+ return supplicantIface.value;
+ }
+ }
+
+ /**
+ * Teardown a STA interface for the specified iface name.
+ *
+ * @param ifaceName Name of the interface.
+ * @return true on success, false otherwise.
+ */
+ public boolean teardownIface(@NonNull String ifaceName) {
+ synchronized (mLock) {
+ if (isV1_1()) {
+ if (!removeIfaceV1_1(ifaceName)) {
+ Log.e(TAG, "Failed to remove iface = " + ifaceName);
+ return false;
+ }
+ }
+ if (mISupplicantStaIfaces.remove(ifaceName) == null) {
+ Log.e(TAG, "Trying to teardown unknown inteface");
return false;
}
- if (!registerCallback(mISupplicantStaIfaceCallback)) {
+ mISupplicantStaIfaceCallbacks.remove(ifaceName);
+ return true;
+ }
+ }
+
+ /**
+ * Remove a STA interface for the specified iface name.
+ *
+ * @param ifaceName Name of the interface.
+ * @return true on success, false otherwise.
+ */
+ private boolean removeIfaceV1_1(@NonNull String ifaceName) {
+ synchronized (mLock) {
+ try {
+ ISupplicant.IfaceInfo ifaceInfo = new ISupplicant.IfaceInfo();
+ ifaceInfo.name = ifaceName;
+ ifaceInfo.type = IfaceType.STA;
+ SupplicantStatus status = getSupplicantMockableV1_1().removeInterface(ifaceInfo);
+ if (status.code != SupplicantStatusCode.SUCCESS) {
+ Log.e(TAG, "Failed to remove iface " + status.code);
+ return false;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "ISupplicant.getInterface exception: " + e);
+ supplicantServiceDiedHandler(ifaceName);
return false;
}
return true;
}
}
- private void supplicantServiceDiedHandler() {
+ /**
+ * Registers a death notification for supplicant.
+ * @return Returns true on success.
+ */
+ public boolean registerDeathHandler(@NonNull SupplicantDeathEventHandler handler) {
+ if (mDeathEventHandler != null) {
+ Log.e(TAG, "Death handler already present");
+ return false;
+ }
+ mDeathEventHandler = handler;
+ return true;
+ }
+
+ /**
+ * Deregisters a death notification for supplicant.
+ * @return Returns true on success.
+ */
+ public boolean deregisterDeathHandler() {
+ if (mDeathEventHandler == null) {
+ Log.e(TAG, "No Death handler present");
+ return false;
+ }
+ mDeathEventHandler = null;
+ return true;
+ }
+
+
+ private void clearState() {
synchronized (mLock) {
mISupplicant = null;
- mISupplicantStaIface = null;
- mWifiMonitor.broadcastSupplicantDisconnectionEvent(mIfaceName);
+ mISupplicantStaIfaces.clear();
+ mCurrentNetworkLocalConfigs.clear();
+ mCurrentNetworkRemoteHandles.clear();
+ }
+ }
+
+ private void supplicantServiceDiedHandler(@NonNull String ifaceName) {
+ synchronized (mLock) {
+ mWifiMonitor.broadcastSupplicantDisconnectionEvent(ifaceName);
+ clearState();
+ }
+ }
+
+ private void supplicantServiceDiedHandler() {
+ synchronized (mLock) {
+ if (mDeathEventHandler != null) {
+ mDeathEventHandler.onDeath();
+ }
+ for (String ifaceName : mISupplicantStaIfaces.keySet()) {
+ mWifiMonitor.broadcastSupplicantDisconnectionEvent(ifaceName);
+ }
+ clearState();
}
}
@@ -359,7 +513,7 @@ public class SupplicantStaIfaceHal {
*/
public boolean isInitializationComplete() {
synchronized (mLock) {
- return mISupplicantStaIface != null;
+ return mISupplicant != null;
}
}
@@ -378,6 +532,14 @@ public class SupplicantStaIfaceHal {
}
}
+ protected android.hardware.wifi.supplicant.V1_1.ISupplicant getSupplicantMockableV1_1()
+ throws RemoteException {
+ synchronized (mLock) {
+ return android.hardware.wifi.supplicant.V1_1.ISupplicant.castFrom(
+ ISupplicant.getService());
+ }
+ }
+
protected ISupplicantStaIface getStaIfaceMockable(ISupplicantIface iface) {
synchronized (mLock) {
return ISupplicantStaIface.asInterface(iface.asBinder());
@@ -385,6 +547,43 @@ public class SupplicantStaIfaceHal {
}
/**
+ * Check if the device is running V1_1 supplicant service.
+ * @return
+ */
+ private boolean isV1_1() {
+ synchronized (mLock) {
+ try {
+ return (getSupplicantMockableV1_1() != null);
+ } catch (RemoteException e) {
+ Log.e(TAG, "ISupplicant.getService exception: " + e);
+ supplicantServiceDiedHandler();
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Helper method to look up the network object for the specified iface.
+ */
+ private ISupplicantStaIface getStaIface(@NonNull String ifaceName) {
+ return mISupplicantStaIfaces.get(ifaceName);
+ }
+
+ /**
+ * Helper method to look up the network object for the specified iface.
+ */
+ private SupplicantStaNetworkHal getCurrentNetworkRemoteHandle(@NonNull String ifaceName) {
+ return mCurrentNetworkRemoteHandles.get(ifaceName);
+ }
+
+ /**
+ * Helper method to look up the network config or the specified iface.
+ */
+ private WifiConfiguration getCurrentNetworkLocalConfig(@NonNull String ifaceName) {
+ return mCurrentNetworkLocalConfigs.get(ifaceName);
+ }
+
+ /**
* Add a network configuration to wpa_supplicant.
*
* @param config Config corresponding to the network.
@@ -392,14 +591,14 @@ public class SupplicantStaIfaceHal {
* for the current network.
*/
private Pair<SupplicantStaNetworkHal, WifiConfiguration>
- addNetworkAndSaveConfig(WifiConfiguration config) {
+ addNetworkAndSaveConfig(@NonNull String ifaceName, WifiConfiguration config) {
synchronized (mLock) {
logi("addSupplicantStaNetwork via HIDL");
if (config == null) {
loge("Cannot add NULL network!");
return null;
}
- SupplicantStaNetworkHal network = addNetwork();
+ SupplicantStaNetworkHal network = addNetwork(ifaceName);
if (network == null) {
loge("Failed to add a network!");
return null;
@@ -412,7 +611,7 @@ public class SupplicantStaIfaceHal {
}
if (!saveSuccess) {
loge("Failed to save variables for: " + config.configKey());
- if (!removeAllNetworks()) {
+ if (!removeAllNetworks(ifaceName)) {
loge("Failed to remove all networks on failure.");
}
return null;
@@ -428,47 +627,50 @@ public class SupplicantStaIfaceHal {
* networks and saves |config|.
* 2. Select the new network in wpa_supplicant.
*
+ * @param ifaceName Name of the interface.
* @param config WifiConfiguration parameters for the provided network.
* @return {@code true} if it succeeds, {@code false} otherwise
*/
- public boolean connectToNetwork(@NonNull WifiConfiguration config) {
+ public boolean connectToNetwork(@NonNull String ifaceName, @NonNull WifiConfiguration config) {
synchronized (mLock) {
logd("connectToNetwork " + config.configKey());
- if (WifiConfigurationUtil.isSameNetwork(config, mCurrentNetworkLocalConfig)) {
+ WifiConfiguration currentConfig = getCurrentNetworkLocalConfig(ifaceName);
+ if (WifiConfigurationUtil.isSameNetwork(config, currentConfig)) {
String networkSelectionBSSID = config.getNetworkSelectionStatus()
.getNetworkSelectionBSSID();
String networkSelectionBSSIDCurrent =
- mCurrentNetworkLocalConfig.getNetworkSelectionStatus()
- .getNetworkSelectionBSSID();
+ currentConfig.getNetworkSelectionStatus().getNetworkSelectionBSSID();
if (Objects.equals(networkSelectionBSSID, networkSelectionBSSIDCurrent)) {
logd("Network is already saved, will not trigger remove and add operation.");
} else {
logd("Network is already saved, but need to update BSSID.");
if (!setCurrentNetworkBssid(
+ ifaceName,
config.getNetworkSelectionStatus().getNetworkSelectionBSSID())) {
loge("Failed to set current network BSSID.");
return false;
}
- mCurrentNetworkLocalConfig = new WifiConfiguration(config);
+ mCurrentNetworkLocalConfigs.put(ifaceName, new WifiConfiguration(config));
}
} else {
- mCurrentNetworkRemoteHandle = null;
- mCurrentNetworkLocalConfig = null;
- if (!removeAllNetworks()) {
+ mCurrentNetworkRemoteHandles.remove(ifaceName);
+ mCurrentNetworkLocalConfigs.remove(ifaceName);
+ if (!removeAllNetworks(ifaceName)) {
loge("Failed to remove existing networks");
return false;
}
Pair<SupplicantStaNetworkHal, WifiConfiguration> pair =
- addNetworkAndSaveConfig(config);
+ addNetworkAndSaveConfig(ifaceName, config);
if (pair == null) {
loge("Failed to add/save network configuration: " + config.configKey());
return false;
}
- mCurrentNetworkRemoteHandle = pair.first;
- mCurrentNetworkLocalConfig = pair.second;
+ mCurrentNetworkRemoteHandles.put(ifaceName, pair.first);
+ mCurrentNetworkLocalConfigs.put(ifaceName, pair.second);
}
-
- if (!mCurrentNetworkRemoteHandle.select()) {
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(ifaceName, "connectToNetwork");
+ if (networkHandle == null || !networkHandle.select()) {
loge("Failed to select network configuration: " + config.configKey());
return false;
}
@@ -485,23 +687,27 @@ public class SupplicantStaIfaceHal {
* 2. Set the new bssid for the network in wpa_supplicant.
* 3. Trigger reassociate command to wpa_supplicant.
*
+ * @param ifaceName Name of the interface.
* @param config WifiConfiguration parameters for the provided network.
* @return {@code true} if it succeeds, {@code false} otherwise
*/
- public boolean roamToNetwork(WifiConfiguration config) {
+ public boolean roamToNetwork(@NonNull String ifaceName, WifiConfiguration config) {
synchronized (mLock) {
- if (getCurrentNetworkId() != config.networkId) {
+ if (getCurrentNetworkId(ifaceName) != config.networkId) {
Log.w(TAG, "Cannot roam to a different network, initiate new connection. "
- + "Current network ID: " + getCurrentNetworkId());
- return connectToNetwork(config);
+ + "Current network ID: " + getCurrentNetworkId(ifaceName));
+ return connectToNetwork(ifaceName, config);
}
String bssid = config.getNetworkSelectionStatus().getNetworkSelectionBSSID();
logd("roamToNetwork" + config.configKey() + " (bssid " + bssid + ")");
- if (!mCurrentNetworkRemoteHandle.setBssid(bssid)) {
+
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(ifaceName, "roamToNetwork");
+ if (networkHandle == null || !networkHandle.setBssid(bssid)) {
loge("Failed to set new bssid on network: " + config.configKey());
return false;
}
- if (!reassociate()) {
+ if (!reassociate(ifaceName)) {
loge("Failed to trigger reassociate");
return false;
}
@@ -512,21 +718,22 @@ public class SupplicantStaIfaceHal {
/**
* Load all the configured networks from wpa_supplicant.
*
+ * @param ifaceName Name of the interface.
* @param configs Map of configuration key to configuration objects corresponding to all
* the networks.
* @param networkExtras Map of extra configuration parameters stored in wpa_supplicant.conf
* @return true if succeeds, false otherwise.
*/
- public boolean loadNetworks(Map<String, WifiConfiguration> configs,
+ public boolean loadNetworks(@NonNull String ifaceName, Map<String, WifiConfiguration> configs,
SparseArray<Map<String, String>> networkExtras) {
synchronized (mLock) {
- List<Integer> networkIds = listNetworks();
+ List<Integer> networkIds = listNetworks(ifaceName);
if (networkIds == null) {
Log.e(TAG, "Failed to list networks");
return false;
}
for (Integer networkId : networkIds) {
- SupplicantStaNetworkHal network = getNetwork(networkId);
+ SupplicantStaNetworkHal network = getNetwork(ifaceName, networkId);
if (network == null) {
Log.e(TAG, "Failed to get network with ID: " + networkId);
return false;
@@ -555,7 +762,7 @@ public class SupplicantStaIfaceHal {
if (duplicateConfig != null) {
// The network is already known. Overwrite the duplicate entry.
Log.i(TAG, "Replacing duplicate network: " + duplicateConfig.networkId);
- removeNetwork(duplicateConfig.networkId);
+ removeNetwork(ifaceName, duplicateConfig.networkId);
networkExtras.remove(duplicateConfig.networkId);
}
}
@@ -567,37 +774,40 @@ public class SupplicantStaIfaceHal {
* Remove the request |networkId| from supplicant if it's the current network,
* if the current configured network matches |networkId|.
*
+ * @param ifaceName Name of the interface.
* @param networkId network id of the network to be removed from supplicant.
*/
- public void removeNetworkIfCurrent(int networkId) {
+ public void removeNetworkIfCurrent(@NonNull String ifaceName, int networkId) {
synchronized (mLock) {
- if (getCurrentNetworkId() == networkId) {
+ if (getCurrentNetworkId(ifaceName) == networkId) {
// Currently we only save 1 network in supplicant.
- removeAllNetworks();
+ removeAllNetworks(ifaceName);
}
}
}
/**
* Remove all networks from supplicant
+ *
+ * @param ifaceName Name of the interface.
*/
- public boolean removeAllNetworks() {
+ public boolean removeAllNetworks(@NonNull String ifaceName) {
synchronized (mLock) {
- ArrayList<Integer> networks = listNetworks();
+ ArrayList<Integer> networks = listNetworks(ifaceName);
if (networks == null) {
Log.e(TAG, "removeAllNetworks failed, got null networks");
return false;
}
for (int id : networks) {
- if (!removeNetwork(id)) {
+ if (!removeNetwork(ifaceName, id)) {
Log.e(TAG, "removeAllNetworks failed to remove network: " + id);
return false;
}
}
// Reset current network info. Probably not needed once we add support to remove/reset
// current network on receiving disconnection event from supplicant (b/32898136).
- mCurrentNetworkLocalConfig = null;
- mCurrentNetworkRemoteHandle = null;
+ mCurrentNetworkRemoteHandles.remove(ifaceName);
+ mCurrentNetworkLocalConfigs.remove(ifaceName);
return true;
}
}
@@ -605,113 +815,152 @@ public class SupplicantStaIfaceHal {
/**
* Set the currently configured network's bssid.
*
+ * @param ifaceName Name of the interface.
* @param bssidStr Bssid to set in the form of "XX:XX:XX:XX:XX:XX"
* @return true if succeeds, false otherwise.
*/
- public boolean setCurrentNetworkBssid(String bssidStr) {
+ public boolean setCurrentNetworkBssid(@NonNull String ifaceName, String bssidStr) {
synchronized (mLock) {
- if (mCurrentNetworkRemoteHandle == null) return false;
- return mCurrentNetworkRemoteHandle.setBssid(bssidStr);
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(ifaceName, "setCurrentNetworkBssid");
+ if (networkHandle == null) return false;
+ return networkHandle.setBssid(bssidStr);
}
}
/**
* Get the currently configured network's WPS NFC token.
*
+ * @param ifaceName Name of the interface.
* @return Hex string corresponding to the WPS NFC token.
*/
- public String getCurrentNetworkWpsNfcConfigurationToken() {
+ public String getCurrentNetworkWpsNfcConfigurationToken(@NonNull String ifaceName) {
synchronized (mLock) {
- if (mCurrentNetworkRemoteHandle == null) return null;
- return mCurrentNetworkRemoteHandle.getWpsNfcConfigurationToken();
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(
+ ifaceName, "getCurrentNetworkWpsNfcConfigurationToken");
+ if (networkHandle == null) return null;
+ return networkHandle.getWpsNfcConfigurationToken();
}
}
/**
* Get the eap anonymous identity for the currently configured network.
*
+ * @param ifaceName Name of the interface.
* @return anonymous identity string if succeeds, null otherwise.
*/
- public String getCurrentNetworkEapAnonymousIdentity() {
+ public String getCurrentNetworkEapAnonymousIdentity(@NonNull String ifaceName) {
synchronized (mLock) {
- if (mCurrentNetworkRemoteHandle == null) return null;
- return mCurrentNetworkRemoteHandle.fetchEapAnonymousIdentity();
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(
+ ifaceName, "getCurrentNetworkEapAnonymousIdentity");
+ if (networkHandle == null) return null;
+ return networkHandle.fetchEapAnonymousIdentity();
}
}
/**
* Send the eap identity response for the currently configured network.
*
+ * @param ifaceName Name of the interface.
* @param identityStr String to send.
* @return true if succeeds, false otherwise.
*/
- public boolean sendCurrentNetworkEapIdentityResponse(String identityStr) {
+ public boolean sendCurrentNetworkEapIdentityResponse(
+ @NonNull String ifaceName, String identityStr) {
synchronized (mLock) {
- if (mCurrentNetworkRemoteHandle == null) return false;
- return mCurrentNetworkRemoteHandle.sendNetworkEapIdentityResponse(identityStr);
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(
+ ifaceName, "sendCurrentNetworkEapIdentityResponse");
+ if (networkHandle == null) return false;
+ return networkHandle.sendNetworkEapIdentityResponse(identityStr);
}
}
/**
* Send the eap sim gsm auth response for the currently configured network.
*
+ * @param ifaceName Name of the interface.
* @param paramsStr String to send.
* @return true if succeeds, false otherwise.
*/
- public boolean sendCurrentNetworkEapSimGsmAuthResponse(String paramsStr) {
+ public boolean sendCurrentNetworkEapSimGsmAuthResponse(
+ @NonNull String ifaceName, String paramsStr) {
synchronized (mLock) {
- if (mCurrentNetworkRemoteHandle == null) return false;
- return mCurrentNetworkRemoteHandle.sendNetworkEapSimGsmAuthResponse(paramsStr);
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(
+ ifaceName, "sendCurrentNetworkEapSimGsmAuthResponse");
+ if (networkHandle == null) return false;
+ return networkHandle.sendNetworkEapSimGsmAuthResponse(paramsStr);
}
}
/**
* Send the eap sim gsm auth failure for the currently configured network.
*
+ * @param ifaceName Name of the interface.
* @return true if succeeds, false otherwise.
*/
- public boolean sendCurrentNetworkEapSimGsmAuthFailure() {
+ public boolean sendCurrentNetworkEapSimGsmAuthFailure(@NonNull String ifaceName) {
synchronized (mLock) {
- if (mCurrentNetworkRemoteHandle == null) return false;
- return mCurrentNetworkRemoteHandle.sendNetworkEapSimGsmAuthFailure();
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(
+ ifaceName, "sendCurrentNetworkEapSimGsmAuthFailure");
+ if (networkHandle == null) return false;
+ return networkHandle.sendNetworkEapSimGsmAuthFailure();
}
}
/**
* Send the eap sim umts auth response for the currently configured network.
*
+ * @param ifaceName Name of the interface.
* @param paramsStr String to send.
* @return true if succeeds, false otherwise.
*/
- public boolean sendCurrentNetworkEapSimUmtsAuthResponse(String paramsStr) {
+ public boolean sendCurrentNetworkEapSimUmtsAuthResponse(
+ @NonNull String ifaceName, String paramsStr) {
synchronized (mLock) {
- if (mCurrentNetworkRemoteHandle == null) return false;
- return mCurrentNetworkRemoteHandle.sendNetworkEapSimUmtsAuthResponse(paramsStr);
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(
+ ifaceName, "sendCurrentNetworkEapSimUmtsAuthResponse");
+ if (networkHandle == null) return false;
+ return networkHandle.sendNetworkEapSimUmtsAuthResponse(paramsStr);
}
}
/**
* Send the eap sim umts auts response for the currently configured network.
*
+ * @param ifaceName Name of the interface.
* @param paramsStr String to send.
* @return true if succeeds, false otherwise.
*/
- public boolean sendCurrentNetworkEapSimUmtsAutsResponse(String paramsStr) {
+ public boolean sendCurrentNetworkEapSimUmtsAutsResponse(
+ @NonNull String ifaceName, String paramsStr) {
synchronized (mLock) {
- if (mCurrentNetworkRemoteHandle == null) return false;
- return mCurrentNetworkRemoteHandle.sendNetworkEapSimUmtsAutsResponse(paramsStr);
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(
+ ifaceName, "sendCurrentNetworkEapSimUmtsAutsResponse");
+ if (networkHandle == null) return false;
+ return networkHandle.sendNetworkEapSimUmtsAutsResponse(paramsStr);
}
}
/**
* Send the eap sim umts auth failure for the currently configured network.
*
+ * @param ifaceName Name of the interface.
* @return true if succeeds, false otherwise.
*/
- public boolean sendCurrentNetworkEapSimUmtsAuthFailure() {
+ public boolean sendCurrentNetworkEapSimUmtsAuthFailure(@NonNull String ifaceName) {
synchronized (mLock) {
- if (mCurrentNetworkRemoteHandle == null) return false;
- return mCurrentNetworkRemoteHandle.sendNetworkEapSimUmtsAuthFailure();
+ SupplicantStaNetworkHal networkHandle =
+ checkSupplicantStaNetworkAndLogFailure(
+ ifaceName, "sendCurrentNetworkEapSimUmtsAuthFailure");
+ if (networkHandle == null) return false;
+ return networkHandle.sendNetworkEapSimUmtsAuthFailure();
}
}
@@ -720,13 +969,14 @@ public class SupplicantStaIfaceHal {
*
* @return The ISupplicantNetwork object for the new network, or null if the call fails
*/
- private SupplicantStaNetworkHal addNetwork() {
+ private SupplicantStaNetworkHal addNetwork(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "addNetwork";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return null;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return null;
Mutable<ISupplicantNetwork> newNetwork = new Mutable<>();
try {
- mISupplicantStaIface.addNetwork((SupplicantStatus status,
+ iface.addNetwork((SupplicantStatus status,
ISupplicantNetwork network) -> {
if (checkStatusAndLogFailure(status, methodStr)) {
newNetwork.value = network;
@@ -737,6 +987,7 @@ public class SupplicantStaIfaceHal {
}
if (newNetwork.value != null) {
return getStaNetworkMockable(
+ ifaceName,
ISupplicantStaNetwork.asInterface(newNetwork.value.asBinder()));
} else {
return null;
@@ -749,12 +1000,13 @@ public class SupplicantStaIfaceHal {
*
* @return true if request is sent successfully, false otherwise.
*/
- private boolean removeNetwork(int id) {
+ private boolean removeNetwork(@NonNull String ifaceName, int id) {
synchronized (mLock) {
final String methodStr = "removeNetwork";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.removeNetwork(id);
+ SupplicantStatus status = iface.removeNetwork(id);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -766,15 +1018,16 @@ public class SupplicantStaIfaceHal {
/**
* Use this to mock the creation of SupplicantStaNetworkHal instance.
*
+ * @param ifaceName Name of the interface.
* @param iSupplicantStaNetwork ISupplicantStaNetwork instance retrieved from HIDL.
* @return The ISupplicantNetwork object for the given SupplicantNetworkId int, returns null if
* the call fails
*/
protected SupplicantStaNetworkHal getStaNetworkMockable(
- ISupplicantStaNetwork iSupplicantStaNetwork) {
+ @NonNull String ifaceName, ISupplicantStaNetwork iSupplicantStaNetwork) {
synchronized (mLock) {
SupplicantStaNetworkHal network =
- new SupplicantStaNetworkHal(iSupplicantStaNetwork, mIfaceName, mContext,
+ new SupplicantStaNetworkHal(iSupplicantStaNetwork, ifaceName, mContext,
mWifiMonitor);
if (network != null) {
network.enableVerboseLogging(mVerboseLoggingEnabled);
@@ -787,14 +1040,14 @@ public class SupplicantStaIfaceHal {
* @return The ISupplicantNetwork object for the given SupplicantNetworkId int, returns null if
* the call fails
*/
- private SupplicantStaNetworkHal getNetwork(int id) {
+ private SupplicantStaNetworkHal getNetwork(@NonNull String ifaceName, int id) {
synchronized (mLock) {
final String methodStr = "getNetwork";
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return null;
Mutable<ISupplicantNetwork> gotNetwork = new Mutable<>();
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return null;
try {
- mISupplicantStaIface.getNetwork(id, (SupplicantStatus status,
- ISupplicantNetwork network) -> {
+ iface.getNetwork(id, (SupplicantStatus status, ISupplicantNetwork network) -> {
if (checkStatusAndLogFailure(status, methodStr)) {
gotNetwork.value = network;
}
@@ -804,6 +1057,7 @@ public class SupplicantStaIfaceHal {
}
if (gotNetwork.value != null) {
return getStaNetworkMockable(
+ ifaceName,
ISupplicantStaNetwork.asInterface(gotNetwork.value.asBinder()));
} else {
return null;
@@ -812,12 +1066,13 @@ public class SupplicantStaIfaceHal {
}
/** See ISupplicantStaNetwork.hal for documentation */
- private boolean registerCallback(ISupplicantStaIfaceCallback callback) {
+ private boolean registerCallback(
+ ISupplicantStaIface iface, ISupplicantStaIfaceCallback callback) {
synchronized (mLock) {
final String methodStr = "registerCallback";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.registerCallback(callback);
+ SupplicantStatus status = iface.registerCallback(callback);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -830,14 +1085,14 @@ public class SupplicantStaIfaceHal {
* @return a list of SupplicantNetworkID ints for all networks controlled by supplicant, returns
* null if the call fails
*/
- private java.util.ArrayList<Integer> listNetworks() {
+ private java.util.ArrayList<Integer> listNetworks(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "listNetworks";
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return null;
Mutable<ArrayList<Integer>> networkIdList = new Mutable<>();
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return null;
try {
- mISupplicantStaIface.listNetworks((SupplicantStatus status,
- java.util.ArrayList<Integer> networkIds) -> {
+ iface.listNetworks((SupplicantStatus status, ArrayList<Integer> networkIds) -> {
if (checkStatusAndLogFailure(status, methodStr)) {
networkIdList.value = networkIds;
}
@@ -852,15 +1107,17 @@ public class SupplicantStaIfaceHal {
/**
* Set WPS device name.
*
+ * @param ifaceName Name of the interface.
* @param name String to be set.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setWpsDeviceName(String name) {
+ public boolean setWpsDeviceName(@NonNull String ifaceName, String name) {
synchronized (mLock) {
final String methodStr = "setWpsDeviceName";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setWpsDeviceName(name);
+ SupplicantStatus status = iface.setWpsDeviceName(name);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -872,10 +1129,11 @@ public class SupplicantStaIfaceHal {
/**
* Set WPS device type.
*
+ * @param ifaceName Name of the interface.
* @param typeStr Type specified as a string. Used format: <categ>-<OUI>-<subcateg>
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setWpsDeviceType(String typeStr) {
+ public boolean setWpsDeviceType(@NonNull String ifaceName, String typeStr) {
synchronized (mLock) {
try {
Matcher match = WPS_DEVICE_TYPE_PATTERN.matcher(typeStr);
@@ -892,7 +1150,7 @@ public class SupplicantStaIfaceHal {
byteBuffer.putShort(categ);
byteBuffer.put(oui);
byteBuffer.putShort(subCateg);
- return setWpsDeviceType(bytes);
+ return setWpsDeviceType(ifaceName, bytes);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument " + typeStr, e);
return false;
@@ -900,12 +1158,13 @@ public class SupplicantStaIfaceHal {
}
}
- private boolean setWpsDeviceType(byte[/* 8 */] type) {
+ private boolean setWpsDeviceType(@NonNull String ifaceName, byte[/* 8 */] type) {
synchronized (mLock) {
final String methodStr = "setWpsDeviceType";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setWpsDeviceType(type);
+ SupplicantStatus status = iface.setWpsDeviceType(type);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -917,15 +1176,17 @@ public class SupplicantStaIfaceHal {
/**
* Set WPS manufacturer.
*
+ * @param ifaceName Name of the interface.
* @param manufacturer String to be set.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setWpsManufacturer(String manufacturer) {
+ public boolean setWpsManufacturer(@NonNull String ifaceName, String manufacturer) {
synchronized (mLock) {
final String methodStr = "setWpsManufacturer";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setWpsManufacturer(manufacturer);
+ SupplicantStatus status = iface.setWpsManufacturer(manufacturer);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -937,15 +1198,17 @@ public class SupplicantStaIfaceHal {
/**
* Set WPS model name.
*
+ * @param ifaceName Name of the interface.
* @param modelName String to be set.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setWpsModelName(String modelName) {
+ public boolean setWpsModelName(@NonNull String ifaceName, String modelName) {
synchronized (mLock) {
final String methodStr = "setWpsModelName";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setWpsModelName(modelName);
+ SupplicantStatus status = iface.setWpsModelName(modelName);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -957,15 +1220,17 @@ public class SupplicantStaIfaceHal {
/**
* Set WPS model number.
*
+ * @param ifaceName Name of the interface.
* @param modelNumber String to be set.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setWpsModelNumber(String modelNumber) {
+ public boolean setWpsModelNumber(@NonNull String ifaceName, String modelNumber) {
synchronized (mLock) {
final String methodStr = "setWpsModelNumber";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setWpsModelNumber(modelNumber);
+ SupplicantStatus status = iface.setWpsModelNumber(modelNumber);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -977,15 +1242,17 @@ public class SupplicantStaIfaceHal {
/**
* Set WPS serial number.
*
+ * @param ifaceName Name of the interface.
* @param serialNumber String to be set.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setWpsSerialNumber(String serialNumber) {
+ public boolean setWpsSerialNumber(@NonNull String ifaceName, String serialNumber) {
synchronized (mLock) {
final String methodStr = "setWpsSerialNumber";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setWpsSerialNumber(serialNumber);
+ SupplicantStatus status = iface.setWpsSerialNumber(serialNumber);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -997,26 +1264,28 @@ public class SupplicantStaIfaceHal {
/**
* Set WPS config methods
*
+ * @param ifaceName Name of the interface.
* @param configMethodsStr List of config methods.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setWpsConfigMethods(String configMethodsStr) {
+ public boolean setWpsConfigMethods(@NonNull String ifaceName, String configMethodsStr) {
synchronized (mLock) {
short configMethodsMask = 0;
String[] configMethodsStrArr = configMethodsStr.split("\\s+");
for (int i = 0; i < configMethodsStrArr.length; i++) {
configMethodsMask |= stringToWpsConfigMethod(configMethodsStrArr[i]);
}
- return setWpsConfigMethods(configMethodsMask);
+ return setWpsConfigMethods(ifaceName, configMethodsMask);
}
}
- private boolean setWpsConfigMethods(short configMethods) {
+ private boolean setWpsConfigMethods(@NonNull String ifaceName, short configMethods) {
synchronized (mLock) {
final String methodStr = "setWpsConfigMethods";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setWpsConfigMethods(configMethods);
+ SupplicantStatus status = iface.setWpsConfigMethods(configMethods);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1028,14 +1297,16 @@ public class SupplicantStaIfaceHal {
/**
* Trigger a reassociation even if the iface is currently connected.
*
+ * @param ifaceName Name of the interface.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean reassociate() {
+ public boolean reassociate(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "reassociate";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.reassociate();
+ SupplicantStatus status = iface.reassociate();
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1047,14 +1318,16 @@ public class SupplicantStaIfaceHal {
/**
* Trigger a reconnection if the iface is disconnected.
*
+ * @param ifaceName Name of the interface.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean reconnect() {
+ public boolean reconnect(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "reconnect";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.reconnect();
+ SupplicantStatus status = iface.reconnect();
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1066,14 +1339,16 @@ public class SupplicantStaIfaceHal {
/**
* Trigger a disconnection from the currently connected network.
*
+ * @param ifaceName Name of the interface.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean disconnect() {
+ public boolean disconnect(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "disconnect";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.disconnect();
+ SupplicantStatus status = iface.disconnect();
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1085,15 +1360,17 @@ public class SupplicantStaIfaceHal {
/**
* Enable or disable power save mode.
*
+ * @param ifaceName Name of the interface.
* @param enable true to enable, false to disable.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setPowerSave(boolean enable) {
+ public boolean setPowerSave(@NonNull String ifaceName, boolean enable) {
synchronized (mLock) {
final String methodStr = "setPowerSave";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setPowerSave(enable);
+ SupplicantStatus status = iface.setPowerSave(enable);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1105,13 +1382,15 @@ public class SupplicantStaIfaceHal {
/**
* Initiate TDLS discover with the specified AP.
*
+ * @param ifaceName Name of the interface.
* @param macAddress MAC Address of the AP.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean initiateTdlsDiscover(String macAddress) {
+ public boolean initiateTdlsDiscover(@NonNull String ifaceName, String macAddress) {
synchronized (mLock) {
try {
- return initiateTdlsDiscover(NativeUtil.macAddressToByteArray(macAddress));
+ return initiateTdlsDiscover(
+ ifaceName, NativeUtil.macAddressToByteArray(macAddress));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument " + macAddress, e);
return false;
@@ -1119,12 +1398,13 @@ public class SupplicantStaIfaceHal {
}
}
/** See ISupplicantStaIface.hal for documentation */
- private boolean initiateTdlsDiscover(byte[/* 6 */] macAddress) {
+ private boolean initiateTdlsDiscover(@NonNull String ifaceName, byte[/* 6 */] macAddress) {
synchronized (mLock) {
final String methodStr = "initiateTdlsDiscover";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.initiateTdlsDiscover(macAddress);
+ SupplicantStatus status = iface.initiateTdlsDiscover(macAddress);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1136,13 +1416,14 @@ public class SupplicantStaIfaceHal {
/**
* Initiate TDLS setup with the specified AP.
*
+ * @param ifaceName Name of the interface.
* @param macAddress MAC Address of the AP.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean initiateTdlsSetup(String macAddress) {
+ public boolean initiateTdlsSetup(@NonNull String ifaceName, String macAddress) {
synchronized (mLock) {
try {
- return initiateTdlsSetup(NativeUtil.macAddressToByteArray(macAddress));
+ return initiateTdlsSetup(ifaceName, NativeUtil.macAddressToByteArray(macAddress));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument " + macAddress, e);
return false;
@@ -1150,12 +1431,13 @@ public class SupplicantStaIfaceHal {
}
}
/** See ISupplicantStaIface.hal for documentation */
- private boolean initiateTdlsSetup(byte[/* 6 */] macAddress) {
+ private boolean initiateTdlsSetup(@NonNull String ifaceName, byte[/* 6 */] macAddress) {
synchronized (mLock) {
final String methodStr = "initiateTdlsSetup";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.initiateTdlsSetup(macAddress);
+ SupplicantStatus status = iface.initiateTdlsSetup(macAddress);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1166,13 +1448,15 @@ public class SupplicantStaIfaceHal {
/**
* Initiate TDLS teardown with the specified AP.
+ * @param ifaceName Name of the interface.
* @param macAddress MAC Address of the AP.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean initiateTdlsTeardown(String macAddress) {
+ public boolean initiateTdlsTeardown(@NonNull String ifaceName, String macAddress) {
synchronized (mLock) {
try {
- return initiateTdlsTeardown(NativeUtil.macAddressToByteArray(macAddress));
+ return initiateTdlsTeardown(
+ ifaceName, NativeUtil.macAddressToByteArray(macAddress));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument " + macAddress, e);
return false;
@@ -1181,12 +1465,13 @@ public class SupplicantStaIfaceHal {
}
/** See ISupplicantStaIface.hal for documentation */
- private boolean initiateTdlsTeardown(byte[/* 6 */] macAddress) {
+ private boolean initiateTdlsTeardown(@NonNull String ifaceName, byte[/* 6 */] macAddress) {
synchronized (mLock) {
final String methodStr = "initiateTdlsTeardown";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.initiateTdlsTeardown(macAddress);
+ SupplicantStatus status = iface.initiateTdlsTeardown(macAddress);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1198,16 +1483,19 @@ public class SupplicantStaIfaceHal {
/**
* Request the specified ANQP elements |elements| from the specified AP |bssid|.
*
+ * @param ifaceName Name of the interface.
* @param bssid BSSID of the AP
* @param infoElements ANQP elements to be queried. Refer to ISupplicantStaIface.AnqpInfoId.
* @param hs20SubTypes HS subtypes to be queried. Refer to ISupplicantStaIface.Hs20AnqpSubTypes.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean initiateAnqpQuery(String bssid, ArrayList<Short> infoElements,
+ public boolean initiateAnqpQuery(@NonNull String ifaceName, String bssid,
+ ArrayList<Short> infoElements,
ArrayList<Integer> hs20SubTypes) {
synchronized (mLock) {
try {
return initiateAnqpQuery(
+ ifaceName,
NativeUtil.macAddressToByteArray(bssid), infoElements, hs20SubTypes);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument " + bssid, e);
@@ -1217,14 +1505,15 @@ public class SupplicantStaIfaceHal {
}
/** See ISupplicantStaIface.hal for documentation */
- private boolean initiateAnqpQuery(byte[/* 6 */] macAddress,
+ private boolean initiateAnqpQuery(@NonNull String ifaceName, byte[/* 6 */] macAddress,
java.util.ArrayList<Short> infoElements, java.util.ArrayList<Integer> subTypes) {
synchronized (mLock) {
final String methodStr = "initiateAnqpQuery";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.initiateAnqpQuery(macAddress,
- infoElements, subTypes);
+ SupplicantStatus status = iface.initiateAnqpQuery(
+ macAddress, infoElements, subTypes);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1236,14 +1525,16 @@ public class SupplicantStaIfaceHal {
/**
* Request the specified ANQP ICON from the specified AP |bssid|.
*
+ * @param ifaceName Name of the interface.
* @param bssid BSSID of the AP
* @param fileName Name of the file to request.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean initiateHs20IconQuery(String bssid, String fileName) {
+ public boolean initiateHs20IconQuery(@NonNull String ifaceName, String bssid, String fileName) {
synchronized (mLock) {
try {
- return initiateHs20IconQuery(NativeUtil.macAddressToByteArray(bssid), fileName);
+ return initiateHs20IconQuery(
+ ifaceName, NativeUtil.macAddressToByteArray(bssid), fileName);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument " + bssid, e);
return false;
@@ -1252,13 +1543,14 @@ public class SupplicantStaIfaceHal {
}
/** See ISupplicantStaIface.hal for documentation */
- private boolean initiateHs20IconQuery(byte[/* 6 */] macAddress, String fileName) {
+ private boolean initiateHs20IconQuery(@NonNull String ifaceName,
+ byte[/* 6 */] macAddress, String fileName) {
synchronized (mLock) {
final String methodStr = "initiateHs20IconQuery";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.initiateHs20IconQuery(macAddress,
- fileName);
+ SupplicantStatus status = iface.initiateHs20IconQuery(macAddress, fileName);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1270,15 +1562,17 @@ public class SupplicantStaIfaceHal {
/**
* Makes a callback to HIDL to getMacAddress from supplicant
*
+ * @param ifaceName Name of the interface.
* @return string containing the MAC address, or null on a failed call
*/
- public String getMacAddress() {
+ public String getMacAddress(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "getMacAddress";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return null;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return null;
Mutable<String> gotMac = new Mutable<>();
try {
- mISupplicantStaIface.getMacAddress((SupplicantStatus status,
+ iface.getMacAddress((SupplicantStatus status,
byte[/* 6 */] macAddr) -> {
if (checkStatusAndLogFailure(status, methodStr)) {
gotMac.value = NativeUtil.macAddressFromByteArray(macAddr);
@@ -1294,14 +1588,16 @@ public class SupplicantStaIfaceHal {
/**
* Start using the added RX filters.
*
+ * @param ifaceName Name of the interface.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean startRxFilter() {
+ public boolean startRxFilter(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "startRxFilter";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.startRxFilter();
+ SupplicantStatus status = iface.startRxFilter();
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1313,14 +1609,16 @@ public class SupplicantStaIfaceHal {
/**
* Stop using the added RX filters.
*
+ * @param ifaceName Name of the interface.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean stopRxFilter() {
+ public boolean stopRxFilter(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "stopRxFilter";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.stopRxFilter();
+ SupplicantStatus status = iface.stopRxFilter();
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1332,11 +1630,12 @@ public class SupplicantStaIfaceHal {
/**
* Add an RX filter.
*
+ * @param ifaceName Name of the interface.
* @param type one of {@link WifiNative#RX_FILTER_TYPE_V4_MULTICAST}
* {@link WifiNative#RX_FILTER_TYPE_V6_MULTICAST} values.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean addRxFilter(int type) {
+ public boolean addRxFilter(@NonNull String ifaceName, int type) {
synchronized (mLock) {
byte halType;
switch (type) {
@@ -1350,16 +1649,17 @@ public class SupplicantStaIfaceHal {
Log.e(TAG, "Invalid Rx Filter type: " + type);
return false;
}
- return addRxFilter(halType);
+ return addRxFilter(ifaceName, halType);
}
}
- public boolean addRxFilter(byte type) {
+ private boolean addRxFilter(@NonNull String ifaceName, byte type) {
synchronized (mLock) {
final String methodStr = "addRxFilter";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.addRxFilter(type);
+ SupplicantStatus status = iface.addRxFilter(type);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1371,11 +1671,12 @@ public class SupplicantStaIfaceHal {
/**
* Remove an RX filter.
*
+ * @param ifaceName Name of the interface.
* @param type one of {@link WifiNative#RX_FILTER_TYPE_V4_MULTICAST}
* {@link WifiNative#RX_FILTER_TYPE_V6_MULTICAST} values.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean removeRxFilter(int type) {
+ public boolean removeRxFilter(@NonNull String ifaceName, int type) {
synchronized (mLock) {
byte halType;
switch (type) {
@@ -1389,16 +1690,17 @@ public class SupplicantStaIfaceHal {
Log.e(TAG, "Invalid Rx Filter type: " + type);
return false;
}
- return removeRxFilter(halType);
+ return removeRxFilter(ifaceName, halType);
}
}
- public boolean removeRxFilter(byte type) {
+ private boolean removeRxFilter(@NonNull String ifaceName, byte type) {
synchronized (mLock) {
final String methodStr = "removeRxFilter";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.removeRxFilter(type);
+ SupplicantStatus status = iface.removeRxFilter(type);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1410,12 +1712,13 @@ public class SupplicantStaIfaceHal {
/**
* Set Bt co existense mode.
*
+ * @param ifaceName Name of the interface.
* @param mode one of the above {@link WifiNative#BLUETOOTH_COEXISTENCE_MODE_DISABLED},
* {@link WifiNative#BLUETOOTH_COEXISTENCE_MODE_ENABLED} or
* {@link WifiNative#BLUETOOTH_COEXISTENCE_MODE_SENSE}.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setBtCoexistenceMode(int mode) {
+ public boolean setBtCoexistenceMode(@NonNull String ifaceName, int mode) {
synchronized (mLock) {
byte halMode;
switch (mode) {
@@ -1432,16 +1735,17 @@ public class SupplicantStaIfaceHal {
Log.e(TAG, "Invalid Bt Coex mode: " + mode);
return false;
}
- return setBtCoexistenceMode(halMode);
+ return setBtCoexistenceMode(ifaceName, halMode);
}
}
- private boolean setBtCoexistenceMode(byte mode) {
+ private boolean setBtCoexistenceMode(@NonNull String ifaceName, byte mode) {
synchronized (mLock) {
final String methodStr = "setBtCoexistenceMode";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setBtCoexistenceMode(mode);
+ SupplicantStatus status = iface.setBtCoexistenceMode(mode);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1452,16 +1756,18 @@ public class SupplicantStaIfaceHal {
/** Enable or disable BT coexistence mode.
*
+ * @param ifaceName Name of the interface.
* @param enable true to enable, false to disable.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setBtCoexistenceScanModeEnabled(boolean enable) {
+ public boolean setBtCoexistenceScanModeEnabled(@NonNull String ifaceName, boolean enable) {
synchronized (mLock) {
final String methodStr = "setBtCoexistenceScanModeEnabled";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
SupplicantStatus status =
- mISupplicantStaIface.setBtCoexistenceScanModeEnabled(enable);
+ iface.setBtCoexistenceScanModeEnabled(enable);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1473,15 +1779,17 @@ public class SupplicantStaIfaceHal {
/**
* Enable or disable suspend mode optimizations.
*
+ * @param ifaceName Name of the interface.
* @param enable true to enable, false otherwise.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setSuspendModeEnabled(boolean enable) {
+ public boolean setSuspendModeEnabled(@NonNull String ifaceName, boolean enable) {
synchronized (mLock) {
final String methodStr = "setSuspendModeEnabled";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setSuspendModeEnabled(enable);
+ SupplicantStatus status = iface.setSuspendModeEnabled(enable);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1493,23 +1801,25 @@ public class SupplicantStaIfaceHal {
/**
* Set country code.
*
+ * @param ifaceName Name of the interface.
* @param codeStr 2 byte ASCII string. For ex: US, CA.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setCountryCode(String codeStr) {
+ public boolean setCountryCode(@NonNull String ifaceName, String codeStr) {
synchronized (mLock) {
if (TextUtils.isEmpty(codeStr)) return false;
- return setCountryCode(NativeUtil.stringToByteArray(codeStr));
+ return setCountryCode(ifaceName, NativeUtil.stringToByteArray(codeStr));
}
}
/** See ISupplicantStaIface.hal for documentation */
- private boolean setCountryCode(byte[/* 2 */] code) {
+ private boolean setCountryCode(@NonNull String ifaceName, byte[/* 2 */] code) {
synchronized (mLock) {
final String methodStr = "setCountryCode";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setCountryCode(code);
+ SupplicantStatus status = iface.setCountryCode(code);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1521,15 +1831,17 @@ public class SupplicantStaIfaceHal {
/**
* Start WPS pin registrar operation with the specified peer and pin.
*
+ * @param ifaceName Name of the interface.
* @param bssidStr BSSID of the peer.
* @param pin Pin to be used.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean startWpsRegistrar(String bssidStr, String pin) {
+ public boolean startWpsRegistrar(@NonNull String ifaceName, String bssidStr, String pin) {
synchronized (mLock) {
if (TextUtils.isEmpty(bssidStr) || TextUtils.isEmpty(pin)) return false;
try {
- return startWpsRegistrar(NativeUtil.macAddressToByteArray(bssidStr), pin);
+ return startWpsRegistrar(
+ ifaceName, NativeUtil.macAddressToByteArray(bssidStr), pin);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument " + bssidStr, e);
return false;
@@ -1538,12 +1850,13 @@ public class SupplicantStaIfaceHal {
}
/** See ISupplicantStaIface.hal for documentation */
- private boolean startWpsRegistrar(byte[/* 6 */] bssid, String pin) {
+ private boolean startWpsRegistrar(@NonNull String ifaceName, byte[/* 6 */] bssid, String pin) {
synchronized (mLock) {
final String methodStr = "startWpsRegistrar";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.startWpsRegistrar(bssid, pin);
+ SupplicantStatus status = iface.startWpsRegistrar(bssid, pin);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1555,13 +1868,14 @@ public class SupplicantStaIfaceHal {
/**
* Start WPS pin display operation with the specified peer.
*
+ * @param ifaceName Name of the interface.
* @param bssidStr BSSID of the peer. Use empty bssid to indicate wildcard.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean startWpsPbc(String bssidStr) {
+ public boolean startWpsPbc(@NonNull String ifaceName, String bssidStr) {
synchronized (mLock) {
try {
- return startWpsPbc(NativeUtil.macAddressToByteArray(bssidStr));
+ return startWpsPbc(ifaceName, NativeUtil.macAddressToByteArray(bssidStr));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument " + bssidStr, e);
return false;
@@ -1570,12 +1884,13 @@ public class SupplicantStaIfaceHal {
}
/** See ISupplicantStaIface.hal for documentation */
- private boolean startWpsPbc(byte[/* 6 */] bssid) {
+ private boolean startWpsPbc(@NonNull String ifaceName, byte[/* 6 */] bssid) {
synchronized (mLock) {
final String methodStr = "startWpsPbc";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.startWpsPbc(bssid);
+ SupplicantStatus status = iface.startWpsPbc(bssid);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1587,16 +1902,18 @@ public class SupplicantStaIfaceHal {
/**
* Start WPS pin keypad operation with the specified pin.
*
+ * @param ifaceName Name of the interface.
* @param pin Pin to be used.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean startWpsPinKeypad(String pin) {
+ public boolean startWpsPinKeypad(@NonNull String ifaceName, String pin) {
if (TextUtils.isEmpty(pin)) return false;
synchronized (mLock) {
final String methodStr = "startWpsPinKeypad";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.startWpsPinKeypad(pin);
+ SupplicantStatus status = iface.startWpsPinKeypad(pin);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1608,13 +1925,14 @@ public class SupplicantStaIfaceHal {
/**
* Start WPS pin display operation with the specified peer.
*
+ * @param ifaceName Name of the interface.
* @param bssidStr BSSID of the peer. Use empty bssid to indicate wildcard.
* @return new pin generated on success, null otherwise.
*/
- public String startWpsPinDisplay(String bssidStr) {
+ public String startWpsPinDisplay(@NonNull String ifaceName, String bssidStr) {
synchronized (mLock) {
try {
- return startWpsPinDisplay(NativeUtil.macAddressToByteArray(bssidStr));
+ return startWpsPinDisplay(ifaceName, NativeUtil.macAddressToByteArray(bssidStr));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Illegal argument " + bssidStr, e);
return null;
@@ -1623,13 +1941,14 @@ public class SupplicantStaIfaceHal {
}
/** See ISupplicantStaIface.hal for documentation */
- private String startWpsPinDisplay(byte[/* 6 */] bssid) {
+ private String startWpsPinDisplay(@NonNull String ifaceName, byte[/* 6 */] bssid) {
synchronized (mLock) {
final String methodStr = "startWpsPinDisplay";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return null;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return null;
final Mutable<String> gotPin = new Mutable<>();
try {
- mISupplicantStaIface.startWpsPinDisplay(bssid,
+ iface.startWpsPinDisplay(bssid,
(SupplicantStatus status, String pin) -> {
if (checkStatusAndLogFailure(status, methodStr)) {
gotPin.value = pin;
@@ -1645,14 +1964,16 @@ public class SupplicantStaIfaceHal {
/**
* Cancels any ongoing WPS requests.
*
+ * @param ifaceName Name of the interface.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean cancelWps() {
+ public boolean cancelWps(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "cancelWps";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.cancelWps();
+ SupplicantStatus status = iface.cancelWps();
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1664,15 +1985,17 @@ public class SupplicantStaIfaceHal {
/**
* Sets whether to use external sim for SIM/USIM processing.
*
+ * @param ifaceName Name of the interface.
* @param useExternalSim true to enable, false otherwise.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setExternalSim(boolean useExternalSim) {
+ public boolean setExternalSim(@NonNull String ifaceName, boolean useExternalSim) {
synchronized (mLock) {
final String methodStr = "setExternalSim";
- if (!checkSupplicantStaIfaceAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.setExternalSim(useExternalSim);
+ SupplicantStatus status = iface.setExternalSim(useExternalSim);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1682,12 +2005,13 @@ public class SupplicantStaIfaceHal {
}
/** See ISupplicant.hal for documentation */
- public boolean enableAutoReconnect(boolean enable) {
+ public boolean enableAutoReconnect(@NonNull String ifaceName, boolean enable) {
synchronized (mLock) {
final String methodStr = "enableAutoReconnect";
- if (!checkSupplicantAndLogFailure(methodStr)) return false;
+ ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
+ if (iface == null) return false;
try {
- SupplicantStatus status = mISupplicantStaIface.enableAutoReconnect(enable);
+ SupplicantStatus status = iface.enableAutoReconnect(enable);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
@@ -1775,13 +2099,30 @@ public class SupplicantStaIfaceHal {
/**
* Returns false if SupplicantStaIface is null, and logs failure to call methodStr
*/
- private boolean checkSupplicantStaIfaceAndLogFailure(final String methodStr) {
+ private ISupplicantStaIface checkSupplicantStaIfaceAndLogFailure(
+ @NonNull String ifaceName, final String methodStr) {
synchronized (mLock) {
- if (mISupplicantStaIface == null) {
+ ISupplicantStaIface iface = getStaIface(ifaceName);
+ if (iface == null) {
Log.e(TAG, "Can't call " + methodStr + ", ISupplicantStaIface is null");
- return false;
+ return null;
}
- return true;
+ return iface;
+ }
+ }
+
+ /**
+ * Returns false if SupplicantStaNetwork is null, and logs failure to call methodStr
+ */
+ private SupplicantStaNetworkHal checkSupplicantStaNetworkAndLogFailure(
+ @NonNull String ifaceName, final String methodStr) {
+ synchronized (mLock) {
+ SupplicantStaNetworkHal networkHal = getCurrentNetworkRemoteHandle(ifaceName);
+ if (networkHal == null) {
+ Log.e(TAG, "Can't call " + methodStr + ", SupplicantStaNetwork is null");
+ return null;
+ }
+ return networkHal;
}
}
@@ -1938,8 +2279,13 @@ public class SupplicantStaIfaceHal {
}
private class SupplicantStaIfaceHalCallback extends ISupplicantStaIfaceCallback.Stub {
+ private String mIfaceName;
private boolean mStateIsFourway = false; // Used to help check for PSK password mismatch
+ SupplicantStaIfaceHalCallback(@NonNull String ifaceName) {
+ mIfaceName = ifaceName;
+ }
+
/**
* Parses the provided payload into an ANQP element.
*
@@ -2008,10 +2354,11 @@ public class SupplicantStaIfaceHal {
mStateIsFourway = (newState == ISupplicantStaIfaceCallback.State.FOURWAY_HANDSHAKE);
if (newSupplicantState == SupplicantState.COMPLETED) {
mWifiMonitor.broadcastNetworkConnectionEvent(
- mIfaceName, getCurrentNetworkId(), bssidStr);
+ mIfaceName, getCurrentNetworkId(mIfaceName), bssidStr);
}
mWifiMonitor.broadcastSupplicantStateChangeEvent(
- mIfaceName, getCurrentNetworkId(), wifiSsid, bssidStr, newSupplicantState);
+ mIfaceName, getCurrentNetworkId(mIfaceName), wifiSsid,
+ bssidStr, newSupplicantState);
}
}
diff --git a/com/android/server/wifi/VelocityBasedConnectedScore.java b/com/android/server/wifi/VelocityBasedConnectedScore.java
index 69643ce9..bfc51f69 100644
--- a/com/android/server/wifi/VelocityBasedConnectedScore.java
+++ b/com/android/server/wifi/VelocityBasedConnectedScore.java
@@ -91,15 +91,14 @@ public class VelocityBasedConnectedScore extends ConnectedScore {
double initialVariance = 9.0 * standardDeviation * standardDeviation;
mFilter.mx = new Matrix(1, new double[]{rssi, 0.0});
mFilter.mP = new Matrix(2, new double[]{initialVariance, 0.0, 0.0, 0.0});
- mLastMillis = millis;
- return;
+ } else {
+ double dt = (millis - mLastMillis) * 0.001;
+ mFilter.mR.put(0, 0, standardDeviation * standardDeviation);
+ setDeltaTimeSeconds(dt);
+ mFilter.predict();
+ mFilter.update(new Matrix(1, new double[]{rssi}));
}
- double dt = (millis - mLastMillis) * 0.001;
- mFilter.mR.put(0, 0, standardDeviation * standardDeviation);
- setDeltaTimeSeconds(dt);
- mFilter.predict();
mLastMillis = millis;
- mFilter.update(new Matrix(1, new double[]{rssi}));
mFilteredRssi = mFilter.mx.get(0, 0);
mEstimatedRateOfRssiChange = mFilter.mx.get(1, 0);
}
@@ -146,6 +145,8 @@ public class VelocityBasedConnectedScore extends ConnectedScore {
return mThresholdMinimumRssi + mThresholdAdjustment;
}
+ private double mMinimumPpsForMeasuringSuccess = 2.0;
+
/**
* Adjusts the threshold if appropriate
* <p>
@@ -159,11 +160,12 @@ public class VelocityBasedConnectedScore extends ConnectedScore {
if (mThresholdAdjustment < -7) return;
if (mFilteredRssi >= getAdjustedRssiThreshold() + 2.0) return;
if (Math.abs(mEstimatedRateOfRssiChange) >= 0.2) return;
- if (wifiInfo.txSuccessRate < 10) return;
- if (wifiInfo.rxSuccessRate < 10) return;
- double probabilityOfSuccessfulTx = (
- wifiInfo.txSuccessRate / (wifiInfo.txSuccessRate + wifiInfo.txBadRate)
- );
+ double txSuccessPps = wifiInfo.txSuccessRate;
+ double rxSuccessPps = wifiInfo.rxSuccessRate;
+ if (txSuccessPps < mMinimumPpsForMeasuringSuccess) return;
+ if (rxSuccessPps < mMinimumPpsForMeasuringSuccess) return;
+ double txBadPps = wifiInfo.txBadRate;
+ double probabilityOfSuccessfulTx = txSuccessPps / (txSuccessPps + txBadPps);
if (probabilityOfSuccessfulTx >= 0.2) {
// May want this amount to vary with how close to threshold we are
mThresholdAdjustment -= 0.5;
diff --git a/com/android/server/wifi/WakeupConfigStoreData.java b/com/android/server/wifi/WakeupConfigStoreData.java
new file mode 100644
index 00000000..57751177
--- /dev/null
+++ b/com/android/server/wifi/WakeupConfigStoreData.java
@@ -0,0 +1,179 @@
+/*
+ * 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 com.android.server.wifi;
+
+import android.util.ArraySet;
+
+import com.android.server.wifi.WifiConfigStore.StoreData;
+import com.android.server.wifi.util.XmlUtil;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Config store data for Wifi Wake.
+ */
+public class WakeupConfigStoreData implements StoreData {
+ private static final String TAG = "WakeupConfigStoreData";
+
+ private static final String XML_TAG_IS_ACTIVE = "IsActive";
+ private static final String XML_TAG_NETWORK_SECTION = "Network";
+ private static final String XML_TAG_SSID = "SSID";
+ private static final String XML_TAG_SECURITY = "Security";
+
+ private final DataSource<Boolean> mIsActiveDataSource;
+ private final DataSource<Set<ScanResultMatchInfo>> mNetworkDataSource;
+
+ /**
+ * Interface defining a data source for the store data.
+ *
+ * @param <T> Type of data source
+ */
+ public interface DataSource<T> {
+ /**
+ * Returns the data from the data source.
+ */
+ T getData();
+
+ /**
+ * Updates the data in the data source.
+ *
+ * @param data Data retrieved from the store
+ */
+ void setData(T data);
+ }
+
+ /**
+ * Creates the config store data with its data sources.
+ *
+ * @param isActiveDataSource Data source for isActive
+ * @param networkDataSource Data source for the locked network list
+ */
+ public WakeupConfigStoreData(
+ DataSource<Boolean> isActiveDataSource,
+ DataSource<Set<ScanResultMatchInfo>> networkDataSource) {
+ mIsActiveDataSource = isActiveDataSource;
+ mNetworkDataSource = networkDataSource;
+ }
+
+ @Override
+ public void serializeData(XmlSerializer out, boolean shared)
+ throws XmlPullParserException, IOException {
+ if (shared) {
+ throw new XmlPullParserException("Share data not supported");
+ }
+
+ XmlUtil.writeNextValue(out, XML_TAG_IS_ACTIVE, mIsActiveDataSource.getData());
+
+ for (ScanResultMatchInfo scanResultMatchInfo : mNetworkDataSource.getData()) {
+ writeNetwork(out, scanResultMatchInfo);
+ }
+ }
+
+ /**
+ * Writes a {@link ScanResultMatchInfo} to an XML output stream.
+ *
+ * @param out XML output stream
+ * @param scanResultMatchInfo The ScanResultMatchInfo to serizialize
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private void writeNetwork(XmlSerializer out, ScanResultMatchInfo scanResultMatchInfo)
+ throws XmlPullParserException, IOException {
+ XmlUtil.writeNextSectionStart(out, XML_TAG_NETWORK_SECTION);
+
+ XmlUtil.writeNextValue(out, XML_TAG_SSID, scanResultMatchInfo.networkSsid);
+ XmlUtil.writeNextValue(out, XML_TAG_SECURITY, scanResultMatchInfo.networkType);
+
+ XmlUtil.writeNextSectionEnd(out, XML_TAG_NETWORK_SECTION);
+ }
+
+ @Override
+ public void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)
+ throws XmlPullParserException, IOException {
+ if (shared) {
+ throw new XmlPullParserException("Shared data not supported");
+ }
+
+ boolean isActive = (Boolean) XmlUtil.readNextValueWithName(in, XML_TAG_IS_ACTIVE);
+ mIsActiveDataSource.setData(isActive);
+
+ Set<ScanResultMatchInfo> networks = new ArraySet<>();
+ while (XmlUtil.gotoNextSectionWithNameOrEnd(in, XML_TAG_NETWORK_SECTION, outerTagDepth)) {
+ networks.add(parseNetwork(in, outerTagDepth + 1));
+ }
+
+ mNetworkDataSource.setData(networks);
+ }
+
+ /**
+ * Parses a {@link ScanResultMatchInfo} from an XML input stream.
+ *
+ * @param in XML input stream
+ * @param outerTagDepth XML tag depth of the containing section
+ * @return The {@link ScanResultMatchInfo}
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ private ScanResultMatchInfo parseNetwork(XmlPullParser in, int outerTagDepth)
+ throws IOException, XmlPullParserException {
+ ScanResultMatchInfo scanResultMatchInfo = new ScanResultMatchInfo();
+ while (!XmlUtil.isNextSectionEnd(in, outerTagDepth)) {
+ String[] valueName = new String[1];
+ Object value = XmlUtil.readCurrentValue(in, valueName);
+ if (valueName[0] == null) {
+ throw new XmlPullParserException("Missing value name");
+ }
+ switch (valueName[0]) {
+ case XML_TAG_SSID:
+ scanResultMatchInfo.networkSsid = (String) value;
+ break;
+ case XML_TAG_SECURITY:
+ scanResultMatchInfo.networkType = (int) value;
+ break;
+ default:
+ throw new XmlPullParserException("Unknown tag under " + TAG + ": "
+ + valueName[0]);
+ }
+ }
+
+ return scanResultMatchInfo;
+ }
+
+ @Override
+ public void resetData(boolean shared) {
+ if (!shared) {
+ mNetworkDataSource.setData(Collections.emptySet());
+ mIsActiveDataSource.setData(false);
+ }
+ }
+
+ @Override
+ public String getName() {
+ return TAG;
+ }
+
+ @Override
+ public boolean supportShareData() {
+ return false;
+ }
+}
diff --git a/com/android/server/wifi/WakeupController.java b/com/android/server/wifi/WakeupController.java
index a3c095ac..4e84c4ae 100644
--- a/com/android/server/wifi/WakeupController.java
+++ b/com/android/server/wifi/WakeupController.java
@@ -18,12 +18,23 @@ package com.android.server.wifi;
import android.content.Context;
import android.database.ContentObserver;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiScanner;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+
/**
* WakeupController is responsible managing Auto Wifi.
*
@@ -31,6 +42,8 @@ import com.android.internal.annotations.VisibleForTesting;
*/
public class WakeupController {
+ private static final String TAG = "WakeupController";
+
// TODO(b/69624403) propagate this to Settings
private static final boolean USE_PLATFORM_WIFI_WAKE = false;
@@ -38,17 +51,57 @@ public class WakeupController {
private final Handler mHandler;
private final FrameworkFacade mFrameworkFacade;
private final ContentObserver mContentObserver;
+ private final WakeupLock mWakeupLock;
+ private final WifiConfigManager mWifiConfigManager;
+ private final WifiInjector mWifiInjector;
+
+ private final WifiScanner.ScanListener mScanListener = new WifiScanner.ScanListener() {
+ @Override
+ public void onPeriodChanged(int periodInMs) {
+ // no-op
+ }
+
+ @Override
+ public void onResults(WifiScanner.ScanData[] results) {
+ // TODO(easchwar) handle scan results
+ }
+
+ @Override
+ public void onFullResult(ScanResult fullScanResult) {
+ // no-op
+ }
+
+ @Override
+ public void onSuccess() {
+ // no-op
+ }
+
+ @Override
+ public void onFailure(int reason, String description) {
+ Log.e(TAG, "ScanListener onFailure: " + reason + ": " + description);
+ }
+ };
/** Whether this feature is enabled in Settings. */
private boolean mWifiWakeupEnabled;
+ /** Whether the WakeupController is currently active. */
+ private boolean mIsActive = false;
+
public WakeupController(
Context context,
Looper looper,
+ WakeupLock wakeupLock,
+ WifiConfigManager wifiConfigManager,
+ WifiConfigStore wifiConfigStore,
+ WifiInjector wifiInjector,
FrameworkFacade frameworkFacade) {
mContext = context;
mHandler = new Handler(looper);
+ mWakeupLock = wakeupLock;
+ mWifiConfigManager = wifiConfigManager;
mFrameworkFacade = frameworkFacade;
+ mWifiInjector = wifiInjector;
mContentObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
@@ -59,6 +112,93 @@ public class WakeupController {
mFrameworkFacade.registerContentObserver(mContext, Settings.Global.getUriFor(
Settings.Global.WIFI_WAKEUP_ENABLED), true, mContentObserver);
mContentObserver.onChange(false /* selfChange */);
+
+ // registering the store data here has the effect of reading the persisted value of the
+ // data sources after system boot finishes
+ WakeupConfigStoreData wakeupConfigStoreData =
+ new WakeupConfigStoreData(new IsActiveDataSource(), mWakeupLock.getDataSource());
+ wifiConfigStore.registerStoreData(wakeupConfigStoreData);
+ }
+
+ private void setActive(boolean isActive) {
+ if (mIsActive != isActive) {
+ mIsActive = isActive;
+ mWifiConfigManager.saveToStore(false /* forceWrite */);
+ }
+ }
+
+ /**
+ * Starts listening for incoming scans.
+ *
+ * <p>Should only be called upon entering ScanMode. WakeupController registers its listener with
+ * the WifiScanner. If the WakeupController is already active, then it returns early. Otherwise
+ * it performs its initialization steps and sets {@link #mIsActive} to true.
+ */
+ public void start() {
+ mWifiInjector.getWifiScanner().registerScanListener(mScanListener);
+
+ // If already active, we don't want to re-initialize the lock, so return early.
+ if (mIsActive) {
+ return;
+ }
+ setActive(true);
+
+ if (mWifiWakeupEnabled) {
+ mWakeupLock.initialize(getMostRecentSavedScanResults());
+ }
+ }
+
+ /**
+ * Stops listening for scans.
+ *
+ * <p>Should only be called upon leaving ScanMode. It deregisters the listener from
+ * WifiScanner.
+ */
+ public void stop() {
+ mWifiInjector.getWifiScanner().deregisterScanListener(mScanListener);
+ }
+
+ /** Resets the WakeupController, setting {@link #mIsActive} to false. */
+ public void reset() {
+ setActive(false);
+ }
+
+ /** Returns a list of saved networks from the last full scan. */
+ private Set<ScanResultMatchInfo> getMostRecentSavedScanResults() {
+ Set<ScanResultMatchInfo> goodSavedNetworks = getGoodSavedNetworks();
+
+ List<ScanResult> scanResults = mWifiInjector.getWifiScanner().getSingleScanResults();
+ Set<ScanResultMatchInfo> lastSeenNetworks = new HashSet<>(scanResults.size());
+ for (ScanResult scanResult : scanResults) {
+ lastSeenNetworks.add(ScanResultMatchInfo.fromScanResult(scanResult));
+ }
+
+ lastSeenNetworks.retainAll(goodSavedNetworks);
+ return lastSeenNetworks;
+ }
+
+ /** Returns a filtered list of saved networks from WifiConfigManager. */
+ private Set<ScanResultMatchInfo> getGoodSavedNetworks() {
+ List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks();
+
+ Set<ScanResultMatchInfo> goodSavedNetworks = new HashSet<>(savedNetworks.size());
+ for (WifiConfiguration config : savedNetworks) {
+ if (isWideAreaNetwork(config)
+ || config.hasNoInternetAccess()
+ || config.noInternetAccessExpected
+ || !config.getNetworkSelectionStatus().getHasEverConnected()) {
+ continue;
+ }
+ goodSavedNetworks.add(ScanResultMatchInfo.fromWifiConfiguration(config));
+ }
+
+ Log.d(TAG, "getGoodSavedNetworks: " + goodSavedNetworks.size());
+ return goodSavedNetworks;
+ }
+
+ //TODO(b/69271702) implement WAN filtering
+ private boolean isWideAreaNetwork(WifiConfiguration wifiConfiguration) {
+ return false;
}
/**
@@ -71,4 +211,26 @@ public class WakeupController {
boolean isEnabled() {
return mWifiWakeupEnabled;
}
+
+ /** Dumps wakeup controller state. */
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("Dump of WakeupController");
+ pw.println("mWifiWakeupEnabled: " + mWifiWakeupEnabled);
+ pw.println("USE_PLATFORM_WIFI_WAKE: " + USE_PLATFORM_WIFI_WAKE);
+ pw.println("mIsActive: " + mIsActive);
+ mWakeupLock.dump(fd, pw, args);
+ }
+
+ private class IsActiveDataSource implements WakeupConfigStoreData.DataSource<Boolean> {
+
+ @Override
+ public Boolean getData() {
+ return mIsActive;
+ }
+
+ @Override
+ public void setData(Boolean data) {
+ mIsActive = data;
+ }
+ }
}
diff --git a/com/android/server/wifi/WakeupLock.java b/com/android/server/wifi/WakeupLock.java
new file mode 100644
index 00000000..1fcd9f83
--- /dev/null
+++ b/com/android/server/wifi/WakeupLock.java
@@ -0,0 +1,142 @@
+/*
+ * 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 com.android.server.wifi;
+
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A lock to determine whether Auto Wifi can re-enable Wifi.
+ *
+ * <p>Wakeuplock manages a list of networks to determine whether the device's location has changed.
+ */
+public class WakeupLock {
+
+ private static final String TAG = WakeupLock.class.getSimpleName();
+
+ @VisibleForTesting
+ static final int CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT = 3;
+
+
+ private final WifiConfigManager mWifiConfigManager;
+ private final Map<ScanResultMatchInfo, Integer> mLockedNetworks = new ArrayMap<>();
+
+ public WakeupLock(WifiConfigManager wifiConfigManager) {
+ mWifiConfigManager = wifiConfigManager;
+ }
+
+ /**
+ * Initializes the WakeupLock with the given {@link ScanResultMatchInfo} list.
+ *
+ * <p>This saves the wakeup lock to the store.
+ *
+ * @param scanResultList list of ScanResultMatchInfos to start the lock with
+ */
+ public void initialize(Collection<ScanResultMatchInfo> scanResultList) {
+ mLockedNetworks.clear();
+ for (ScanResultMatchInfo scanResultMatchInfo : scanResultList) {
+ mLockedNetworks.put(scanResultMatchInfo, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
+ }
+
+ Log.d(TAG, "Lock initialized. Number of networks: " + mLockedNetworks.size());
+
+ mWifiConfigManager.saveToStore(false /* forceWrite */);
+ }
+
+ /**
+ * Updates the lock with the given {@link ScanResultMatchInfo} list.
+ *
+ * <p>If a network in the lock is not present in the list, reduce the number of scans
+ * required to evict by one. Remove any entries in the list with 0 scans required to evict. If
+ * any entries in the lock are removed, the store is updated.
+ *
+ * @param scanResultList list of present ScanResultMatchInfos to update the lock with
+ */
+ public void update(Collection<ScanResultMatchInfo> scanResultList) {
+ boolean hasChanged = false;
+ Iterator<Map.Entry<ScanResultMatchInfo, Integer>> it =
+ mLockedNetworks.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<ScanResultMatchInfo, Integer> entry = it.next();
+
+ // if present in scan list, reset to max
+ if (scanResultList.contains(entry.getKey())) {
+ entry.setValue(CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
+ continue;
+ }
+
+ // decrement and remove if necessary
+ entry.setValue(entry.getValue() - 1);
+ if (entry.getValue() <= 0) {
+ it.remove();
+ hasChanged = true;
+ }
+ }
+
+ if (hasChanged) {
+ mWifiConfigManager.saveToStore(false /* forceWrite */);
+ }
+ }
+
+ /**
+ * Returns whether the internal network set is empty.
+ */
+ public boolean isEmpty() {
+ return mLockedNetworks.isEmpty();
+ }
+
+ /** Returns the data source for the WakeupLock config store data. */
+ public WakeupConfigStoreData.DataSource<Set<ScanResultMatchInfo>> getDataSource() {
+ return new WakeupLockDataSource();
+ }
+
+ /** Dumps wakeup lock contents. */
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("WakeupLock: ");
+ pw.println("Locked networks: " + mLockedNetworks.size());
+ for (Map.Entry<ScanResultMatchInfo, Integer> entry : mLockedNetworks.entrySet()) {
+ pw.println(entry.getKey() + ", scans to evict: " + entry.getValue());
+ }
+ }
+
+ private class WakeupLockDataSource
+ implements WakeupConfigStoreData.DataSource<Set<ScanResultMatchInfo>> {
+
+ @Override
+ public Set<ScanResultMatchInfo> getData() {
+ return mLockedNetworks.keySet();
+ }
+
+ @Override
+ public void setData(Set<ScanResultMatchInfo> data) {
+ mLockedNetworks.clear();
+ for (ScanResultMatchInfo network : data) {
+ mLockedNetworks.put(network, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
+ }
+
+ }
+ }
+}
diff --git a/com/android/server/wifi/WifiCertManager.java b/com/android/server/wifi/WifiCertManager.java
deleted file mode 100644
index e180f515..00000000
--- a/com/android/server/wifi/WifiCertManager.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * 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 com.android.server.wifi;
-
-import android.app.admin.IDevicePolicyManager;
-import android.content.Context;
-import android.os.Environment;
-import android.os.ServiceManager;
-import android.os.UserHandle;
-import android.security.Credentials;
-import android.security.KeyStore;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.server.net.DelayedDiskWrite;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Manager class for affiliated Wifi certificates.
- */
-public class WifiCertManager {
- private static final String TAG = "WifiCertManager";
- private static final String SEP = "\n";
-
- private final Context mContext;
- private final Set<String> mAffiliatedUserOnlyCerts = new HashSet<String>();
- private final String mConfigFile;
-
- private static final String CONFIG_FILE =
- Environment.getDataDirectory() + "/misc/wifi/affiliatedcerts.txt";
-
- private final DelayedDiskWrite mWriter = new DelayedDiskWrite();
-
-
- WifiCertManager(Context context) {
- this(context, CONFIG_FILE);
- }
-
- WifiCertManager(Context context, String configFile) {
- mContext = context;
- mConfigFile = configFile;
- final byte[] bytes = readConfigFile();
- if (bytes == null) {
- // Config file does not exist or empty.
- return;
- }
-
- String[] keys = new String(bytes, StandardCharsets.UTF_8).split(SEP);
- for (String key : keys) {
- mAffiliatedUserOnlyCerts.add(key);
- }
-
- // Remove keys that no longer exist in KeyStore.
- if (mAffiliatedUserOnlyCerts.retainAll(Arrays.asList(listClientCertsForAllUsers()))) {
- writeConfig();
- }
- }
-
- /** @param key Unprefixed cert key to hide from unaffiliated users. */
- public void hideCertFromUnaffiliatedUsers(String key) {
- if (mAffiliatedUserOnlyCerts.add(Credentials.USER_PRIVATE_KEY + key)) {
- writeConfig();
- }
- }
-
- /** @return Prefixed cert keys that are visible to the current user. */
- public String[] listClientCertsForCurrentUser() {
- HashSet<String> results = new HashSet<String>();
-
- String[] keys = listClientCertsForAllUsers();
- if (isAffiliatedUser()) {
- return keys;
- }
-
- for (String key : keys) {
- if (!mAffiliatedUserOnlyCerts.contains(key)) {
- results.add(key);
- }
- }
- return results.toArray(new String[results.size()]);
- }
-
- private void writeConfig() {
- String[] values =
- mAffiliatedUserOnlyCerts.toArray(new String[mAffiliatedUserOnlyCerts.size()]);
- String value = TextUtils.join(SEP, values);
- writeConfigFile(value.getBytes(StandardCharsets.UTF_8));
- }
-
- protected byte[] readConfigFile() {
- byte[] bytes = null;
- try {
- final File file = new File(mConfigFile);
- final long fileSize = file.exists() ? file.length() : 0;
- if (fileSize == 0 || fileSize >= Integer.MAX_VALUE) {
- // Config file is empty/corrupted/non-existing.
- return bytes;
- }
-
- bytes = new byte[(int) file.length()];
- final DataInputStream stream = new DataInputStream(new FileInputStream(file));
- stream.readFully(bytes);
- } catch (IOException e) {
- Log.e(TAG, "readConfigFile: failed to read " + e, e);
- }
- return bytes;
- }
-
- protected void writeConfigFile(byte[] payload) {
- final byte[] data = payload;
- mWriter.write(mConfigFile, new DelayedDiskWrite.Writer() {
- public void onWriteCalled(DataOutputStream out) throws IOException {
- out.write(data, 0, data.length);
- }
- });
- }
-
- protected String[] listClientCertsForAllUsers() {
- return KeyStore.getInstance().list(Credentials.USER_PRIVATE_KEY, UserHandle.myUserId());
- }
-
- protected boolean isAffiliatedUser() {
- IDevicePolicyManager pm = IDevicePolicyManager.Stub.asInterface(
- ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
- boolean result = false;
- try {
- result = pm.isAffiliatedUser();
- } catch (Exception e) {
- Log.e(TAG, "failed to check user affiliation", e);
- }
- return result;
- }
-}
diff --git a/com/android/server/wifi/WifiConfigManager.java b/com/android/server/wifi/WifiConfigManager.java
index 8fb61c21..ebd18cd4 100644
--- a/com/android/server/wifi/WifiConfigManager.java
+++ b/com/android/server/wifi/WifiConfigManager.java
@@ -2349,8 +2349,7 @@ public class WifiConfigManager {
Iterator<WifiConfiguration> iter = networks.iterator();
while (iter.hasNext()) {
WifiConfiguration config = iter.next();
- if (!config.hiddenSSID ||
- config.getNetworkSelectionStatus().isNetworkPermanentlyDisabled()) {
+ if (!config.hiddenSSID) {
iter.remove();
}
}
diff --git a/com/android/server/wifi/WifiConnectivityManager.java b/com/android/server/wifi/WifiConnectivityManager.java
index 458f73ae..c67e7c64 100644
--- a/com/android/server/wifi/WifiConnectivityManager.java
+++ b/com/android/server/wifi/WifiConnectivityManager.java
@@ -814,24 +814,38 @@ public class WifiConnectivityManager {
}
}
+ boolean isScanNeeded = true;
boolean isFullBandScan = true;
-
- // If the WiFi traffic is heavy, only partial scan is initiated.
- if (mWifiState == WIFI_STATE_CONNECTED
- && (mWifiInfo.txSuccessRate > mFullScanMaxTxRate
- || mWifiInfo.rxSuccessRate > mFullScanMaxRxRate)) {
- localLog("No full band scan due to ongoing traffic");
- isFullBandScan = false;
+ boolean isTrafficOverThreshold = mWifiInfo.txSuccessRate > mFullScanMaxTxRate
+ || mWifiInfo.rxSuccessRate > mFullScanMaxRxRate;
+
+ // If the WiFi traffic is heavy, only partial scan is proposed.
+ if (mWifiState == WIFI_STATE_CONNECTED && isTrafficOverThreshold) {
+ // If only partial scan is proposed and firmware roaming control is supported,
+ // we will not issue any scan because firmware roaming will take care of
+ // intra-SSID roam.
+ if (mConnectivityHelper.isFirmwareRoamingSupported()) {
+ localLog("No partial scan because firmware roaming is supported.");
+ isScanNeeded = false;
+ } else {
+ localLog("No full band scan due to ongoing traffic");
+ isFullBandScan = false;
+ }
}
- mLastPeriodicSingleScanTimeStamp = currentTimeStamp;
- startSingleScan(isFullBandScan, WIFI_WORK_SOURCE);
- schedulePeriodicScanTimer(mPeriodicSingleScanInterval);
+ if (isScanNeeded) {
+ mLastPeriodicSingleScanTimeStamp = currentTimeStamp;
+ startSingleScan(isFullBandScan, WIFI_WORK_SOURCE);
+ schedulePeriodicScanTimer(mPeriodicSingleScanInterval);
- // Set up the next scan interval in an exponential backoff fashion.
- mPeriodicSingleScanInterval *= 2;
- if (mPeriodicSingleScanInterval > MAX_PERIODIC_SCAN_INTERVAL_MS) {
- mPeriodicSingleScanInterval = MAX_PERIODIC_SCAN_INTERVAL_MS;
+ // Set up the next scan interval in an exponential backoff fashion.
+ mPeriodicSingleScanInterval *= 2;
+ if (mPeriodicSingleScanInterval > MAX_PERIODIC_SCAN_INTERVAL_MS) {
+ mPeriodicSingleScanInterval = MAX_PERIODIC_SCAN_INTERVAL_MS;
+ }
+ } else {
+ // Since we already skipped this scan, keep the same scan interval for next scan.
+ schedulePeriodicScanTimer(mPeriodicSingleScanInterval);
}
}
diff --git a/com/android/server/wifi/WifiController.java b/com/android/server/wifi/WifiController.java
index 494ce86e..dc985955 100644
--- a/com/android/server/wifi/WifiController.java
+++ b/com/android/server/wifi/WifiController.java
@@ -93,6 +93,7 @@ public class WifiController extends StateMachine {
/* References to values tracked in WifiService */
private final WifiStateMachine mWifiStateMachine;
+ private final WifiStateMachinePrime mWifiStateMachinePrime;
private final WifiSettingsStore mSettingsStore;
private final WifiLockManager mWifiLockManager;
@@ -143,11 +144,13 @@ public class WifiController extends StateMachine {
private EcmState mEcmState = new EcmState();
WifiController(Context context, WifiStateMachine wsm, WifiSettingsStore wss,
- WifiLockManager wifiLockManager, Looper looper, FrameworkFacade f) {
+ WifiLockManager wifiLockManager, Looper looper, FrameworkFacade f,
+ WifiStateMachinePrime wsmp) {
super(TAG, looper);
mFacade = f;
mContext = context;
mWifiStateMachine = wsm;
+ mWifiStateMachinePrime = wsmp;
mSettingsStore = wss;
mWifiLockManager = wifiLockManager;
@@ -435,7 +438,8 @@ public class WifiController extends StateMachine {
@Override
public void enter() {
mWifiStateMachine.setSupplicantRunning(false);
- // Supplicant can't restart right away, so not the time we switched off
+ mWifiStateMachinePrime.disableWifi();
+ // Supplicant can't restart right away, so note the time we switched off
mDisabledTimestamp = SystemClock.elapsedRealtime();
mDeferredEnableSerialNumber++;
mHaveDeferredEnable = false;
@@ -481,6 +485,7 @@ public class WifiController extends StateMachine {
}
mWifiStateMachine.setHostApRunning((SoftApModeConfiguration) msg.obj,
true);
+ mWifiStateMachinePrime.enterSoftAPMode((SoftApModeConfiguration) msg.obj);
transitionTo(mApEnabledState);
}
break;
@@ -565,8 +570,13 @@ public class WifiController extends StateMachine {
if (msg.arg1 == 1) {
// remeber that we were enabled
mSettingsStore.setWifiSavedState(WifiSettingsStore.WIFI_ENABLED);
- deferMessage(obtainMessage(msg.what, msg.arg1, 1, msg.obj));
- transitionTo(mApStaDisabledState);
+ mWifiStateMachine.setHostApRunning((SoftApModeConfiguration) msg.obj, true);
+ mWifiStateMachinePrime.enterSoftAPMode((SoftApModeConfiguration) msg.obj);
+ transitionTo(mApEnabledState);
+ // we should just go directly to ApEnabled since we will kill interfaces
+ // from WSMP
+ //deferMessage(obtainMessage(msg.what, msg.arg1, 1, msg.obj));
+ //transitionTo(mApStaDisabledState);
}
break;
default:
@@ -630,8 +640,14 @@ public class WifiController extends StateMachine {
// Before starting tethering, turn off supplicant for scan mode
if (msg.arg1 == 1) {
mSettingsStore.setWifiSavedState(WifiSettingsStore.WIFI_DISABLED);
- deferMessage(obtainMessage(msg.what, msg.arg1, 1, msg.obj));
- transitionTo(mApStaDisabledState);
+
+ mWifiStateMachine.setHostApRunning((SoftApModeConfiguration) msg.obj, true);
+ mWifiStateMachinePrime.enterSoftAPMode((SoftApModeConfiguration) msg.obj);
+ transitionTo(mApEnabledState);
+ // we should just go directly to ApEnabled since we will kill interfaces
+ // from WSMP
+ //deferMessage(obtainMessage(msg.what, msg.arg1, 1, msg.obj));
+ //transitionTo(mApStaDisabledState);
}
break;
case CMD_DEFERRED_TOGGLE:
@@ -701,22 +717,26 @@ public class WifiController extends StateMachine {
case CMD_AIRPLANE_TOGGLED:
if (mSettingsStore.isAirplaneModeOn()) {
mWifiStateMachine.setHostApRunning(null, false);
+ mWifiStateMachinePrime.disableWifi();
mPendingState = mApStaDisabledState;
}
break;
case CMD_WIFI_TOGGLED:
if (mSettingsStore.isWifiToggleEnabled()) {
mWifiStateMachine.setHostApRunning(null, false);
+ mWifiStateMachinePrime.disableWifi();
mPendingState = mDeviceActiveState;
}
break;
case CMD_SET_AP:
if (msg.arg1 == 0) {
mWifiStateMachine.setHostApRunning(null, false);
+ mWifiStateMachinePrime.disableWifi();
mPendingState = getNextWifiState();
}
break;
case CMD_AP_STOPPED:
+ mWifiStateMachine.setHostApRunning(null, false);
if (mPendingState == null) {
/**
* Stop triggered internally, either tether notification
@@ -736,10 +756,12 @@ public class WifiController extends StateMachine {
case CMD_EMERGENCY_MODE_CHANGED:
if (msg.arg1 == 1) {
mWifiStateMachine.setHostApRunning(null, false);
+ mWifiStateMachinePrime.disableWifi();
mPendingState = mEcmState;
}
break;
case CMD_AP_START_FAILURE:
+ mWifiStateMachine.setHostApRunning(null, false);
transitionTo(getNextWifiState());
break;
default:
diff --git a/com/android/server/wifi/WifiCountryCode.java b/com/android/server/wifi/WifiCountryCode.java
index 66a035f0..a9cbed60 100644
--- a/com/android/server/wifi/WifiCountryCode.java
+++ b/com/android/server/wifi/WifiCountryCode.java
@@ -19,6 +19,9 @@ package com.android.server.wifi;
import android.text.TextUtils;
import android.util.Log;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
/**
* Provide functions for making changes to WiFi country code.
* This Country Code is from MCC or phone default setting. This class sends Country Code
@@ -167,6 +170,21 @@ public class WifiCountryCode {
return pickCountryCode();
}
+ /**
+ * Method to dump the current state of this WifiCounrtyCode object.
+ */
+ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mCurrentCountryCode != null) {
+ pw.println("CountryCode sent to driver: " + mCurrentCountryCode);
+ } else {
+ if (pickCountryCode() != null) {
+ pw.println("CountryCode: " + pickCountryCode() + " was not sent to driver");
+ } else {
+ pw.println("CountryCode was not initialized");
+ }
+ }
+ }
+
private void updateCountryCode() {
if (DBG) Log.d(TAG, "Update country code");
String country = pickCountryCode();
diff --git a/com/android/server/wifi/WifiDiagnostics.java b/com/android/server/wifi/WifiDiagnostics.java
index 921faa30..30294bda 100644
--- a/com/android/server/wifi/WifiDiagnostics.java
+++ b/com/android/server/wifi/WifiDiagnostics.java
@@ -454,6 +454,10 @@ class WifiDiagnostics extends BaseWifiDiagnostics {
return false;
}
+ if (!isVerboseLoggingEnabled()) {
+ return false;
+ }
+
for (WifiNative.RingBufferStatus buffer : mRingBuffers){
if ((buffer.flag & RING_BUFFER_FLAG_HAS_PER_PACKET_ENTRIES) != 0) {
diff --git a/com/android/server/wifi/WifiInjector.java b/com/android/server/wifi/WifiInjector.java
index f14a57f6..c2806d88 100644
--- a/com/android/server/wifi/WifiInjector.java
+++ b/com/android/server/wifi/WifiInjector.java
@@ -21,7 +21,6 @@ import android.app.ActivityManager;
import android.content.Context;
import android.net.NetworkKey;
import android.net.NetworkScoreManager;
-import android.net.wifi.IApInterface;
import android.net.wifi.IWifiScanner;
import android.net.wifi.IWificond;
import android.net.wifi.WifiInfo;
@@ -84,8 +83,8 @@ public class WifiInjector {
private final SupplicantP2pIfaceHal mSupplicantP2pIfaceHal;
private final WifiVendorHal mWifiVendorHal;
private final WifiStateMachine mWifiStateMachine;
+ private final WifiStateMachinePrime mWifiStateMachinePrime;
private final WifiSettingsStore mSettingsStore;
- private final WifiCertManager mCertManager;
private final OpenNetworkNotifier mOpenNetworkNotifier;
private final WifiLockManager mLockManager;
private final WifiController mWifiController;
@@ -123,6 +122,7 @@ public class WifiInjector {
private final Runtime mJavaRuntime;
private final SelfRecovery mSelfRecovery;
private final WakeupController mWakeupController;
+ private final INetworkManagementService mNwManagementService;
private final boolean mUseRealLogger;
@@ -170,8 +170,10 @@ public class WifiInjector {
mSupplicantStaIfaceHal = new SupplicantStaIfaceHal(mContext, mWifiMonitor);
mWificondControl = new WificondControl(this, mWifiMonitor,
new CarrierNetworkConfig(mContext));
+ mNwManagementService = INetworkManagementService.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
mWifiNative = new WifiNative(SystemProperties.get("wifi.interface", "wlan0"),
- mWifiVendorHal, mSupplicantStaIfaceHal, mWificondControl);
+ mWifiVendorHal, mSupplicantStaIfaceHal, mWificondControl, mNwManagementService);
mWifiP2pMonitor = new WifiP2pMonitor(this);
mSupplicantP2pIfaceHal = new SupplicantP2pIfaceHal(mWifiP2pMonitor);
mWifiP2pNative = new WifiP2pNative(SystemProperties.get("wifi.direct.interface", "p2p0"),
@@ -225,17 +227,23 @@ public class WifiInjector {
wifiStateMachineLooper, UserManager.get(mContext),
this, mBackupManagerProxy, mCountryCode, mWifiNative,
new WrongPasswordNotifier(mContext, mFrameworkFacade));
- mCertManager = new WifiCertManager(mContext);
+ IBinder b = mFrameworkFacade.getService(Context.NETWORKMANAGEMENT_SERVICE);
+ INetworkManagementService networkManagementService =
+ INetworkManagementService.Stub.asInterface(b);
+ mWifiStateMachinePrime = new WifiStateMachinePrime(this, wifiStateMachineLooper,
+ mWifiNative, networkManagementService);
mOpenNetworkNotifier = new OpenNetworkNotifier(mContext,
mWifiStateMachineHandlerThread.getLooper(), mFrameworkFacade, mClock, mWifiMetrics,
mWifiConfigManager, mWifiConfigStore, mWifiStateMachine,
new OpenNetworkRecommender(),
new ConnectToNetworkNotificationBuilder(mContext, mFrameworkFacade));
mWakeupController = new WakeupController(mContext,
- mWifiStateMachineHandlerThread.getLooper(), mFrameworkFacade);
+ mWifiStateMachineHandlerThread.getLooper(), new WakeupLock(mWifiConfigManager),
+ mWifiConfigManager, mWifiConfigStore, this, mFrameworkFacade);
mLockManager = new WifiLockManager(mContext, BatteryStatsService.getService());
mWifiController = new WifiController(mContext, mWifiStateMachine, mSettingsStore,
- mLockManager, mWifiServiceHandlerThread.getLooper(), mFrameworkFacade);
+ mLockManager, mWifiServiceHandlerThread.getLooper(), mFrameworkFacade,
+ mWifiStateMachinePrime);
mSelfRecovery = new SelfRecovery(mWifiController, mClock);
mWifiLastResortWatchdog = new WifiLastResortWatchdog(mSelfRecovery, mWifiMetrics);
mWifiMulticastLockManager = new WifiMulticastLockManager(mWifiStateMachine,
@@ -256,6 +264,17 @@ public class WifiInjector {
return sWifiInjector;
}
+ /**
+ * Enable verbose logging in Injector objects. Called from the WifiServiceImpl (based on
+ * binder call).
+ */
+ public void enableVerboseLogging(int verbose) {
+ mWifiLastResortWatchdog.enableVerboseLogging(verbose);
+ mWifiBackupRestore.enableVerboseLogging(verbose);
+ mHalDeviceManager.enableVerboseLogging(verbose);
+ LogcatLog.enableVerboseLogging(verbose);
+ }
+
public UserManager getUserManager() {
return UserManager.get(mContext);
}
@@ -300,12 +319,12 @@ public class WifiInjector {
return mWifiStateMachine;
}
- public WifiSettingsStore getWifiSettingsStore() {
- return mSettingsStore;
+ public WifiStateMachinePrime getWifiStateMachinePrime() {
+ return mWifiStateMachinePrime;
}
- public WifiCertManager getWifiCertManager() {
- return mCertManager;
+ public WifiSettingsStore getWifiSettingsStore() {
+ return mSettingsStore;
}
public WifiLockManager getWifiLockManager() {
@@ -376,20 +395,15 @@ public class WifiInjector {
* @param nmService NetworkManagementService allowing SoftApManager to listen for interface
* changes
* @param listener listener for SoftApManager
- * @param apInterface network interface to start hostapd against
- * @param ifaceName name of the ap interface
* @param config SoftApModeConfiguration object holding the config and mode
* @return an instance of SoftApManager
*/
public SoftApManager makeSoftApManager(INetworkManagementService nmService,
SoftApManager.Listener listener,
- @NonNull IApInterface apInterface,
- @NonNull String ifaceName,
@NonNull SoftApModeConfiguration config) {
- return new SoftApManager(mContext, mWifiServiceHandlerThread.getLooper(),
- mWifiNative, mCountryCode.getCountryCode(),
- listener, apInterface, ifaceName, nmService,
- mWifiApConfigStore, config, mWifiMetrics);
+ return new SoftApManager(mContext, mWifiStateMachineHandlerThread.getLooper(),
+ mFrameworkFacade, mWifiNative, mCountryCode.getCountryCode(), listener,
+ nmService, mWifiApConfigStore, config, mWifiMetrics);
}
/**
diff --git a/com/android/server/wifi/WifiLinkLayerStats.java b/com/android/server/wifi/WifiLinkLayerStats.java
new file mode 100644
index 00000000..0c586700
--- /dev/null
+++ b/com/android/server/wifi/WifiLinkLayerStats.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wifi;
+
+import java.util.Arrays;
+
+/**
+ * A class representing link layer statistics collected over a Wifi Interface.
+ */
+
+/**
+ * {@hide}
+ */
+public class WifiLinkLayerStats {
+
+ /** Number of beacons received from our own AP */
+ public int beacon_rx;
+
+ /** RSSI of management frames */
+ public int rssi_mgmt;
+
+ /* Packet counters */
+
+ /** WME Best Effort Access Category received mpdu */
+ public long rxmpdu_be;
+ /** WME Best Effort Access Category transmitted mpdu */
+ public long txmpdu_be;
+ /** WME Best Effort Access Category lost mpdu */
+ public long lostmpdu_be;
+ /** WME Best Effort Access Category number of transmission retries */
+ public long retries_be;
+
+ /** WME Background Access Category received mpdu */
+ public long rxmpdu_bk;
+ /** WME Background Access Category transmitted mpdu */
+ public long txmpdu_bk;
+ /** WME Background Access Category lost mpdu */
+ public long lostmpdu_bk;
+ /** WME Background Access Category number of transmission retries */
+ public long retries_bk;
+
+ /** WME Video Access Category received mpdu */
+ public long rxmpdu_vi;
+ /** WME Video Access Category transmitted mpdu */
+ public long txmpdu_vi;
+ /** WME Video Access Category lost mpdu */
+ public long lostmpdu_vi;
+ /** WME Video Access Category number of transmission retries */
+ public long retries_vi;
+
+ /** WME Voice Access Category received mpdu */
+ public long rxmpdu_vo;
+ /** WME Voice Access Category transmitted mpdu */
+ public long txmpdu_vo;
+ /** WME Voice Access Category lost mpdu */
+ public long lostmpdu_vo;
+ /** WME Voice Access Category number of transmission retries */
+ public long retries_vo;
+
+ /**
+ * Cumulative milliseconds when radio is awake
+ */
+ public int on_time;
+ /**
+ * Cumulative milliseconds of active transmission
+ */
+ public int tx_time;
+ /**
+ * Cumulative milliseconds per level of active transmission
+ */
+ public int[] tx_time_per_level;
+ /**
+ * Cumulative milliseconds of active receive
+ */
+ public int rx_time;
+ /**
+ * Cumulative milliseconds when radio is awake due to scan
+ */
+ public int on_time_scan;
+
+ /**
+ * TimeStamp - absolute milliseconds from boot when these stats were sampled.
+ */
+ public long timeStampInMs;
+
+ @Override
+ public String toString() {
+ StringBuilder sbuf = new StringBuilder();
+ sbuf.append(" WifiLinkLayerStats: ").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));
+ sbuf.append(" ts=" + timeStampInMs);
+ return sbuf.toString();
+ }
+
+}
diff --git a/com/android/server/wifi/WifiNative.java b/com/android/server/wifi/WifiNative.java
index cf80d223..c22b0a51 100644
--- a/com/android/server/wifi/WifiNative.java
+++ b/com/android/server/wifi/WifiNative.java
@@ -16,8 +16,10 @@
package com.android.server.wifi;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.net.InterfaceConfiguration;
import android.net.apf.ApfCapabilities;
import android.net.wifi.IApInterface;
import android.net.wifi.IClientInterface;
@@ -25,10 +27,12 @@ import android.net.wifi.RttManager;
import android.net.wifi.RttManager.ResponderConfig;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiLinkLayerStats;
import android.net.wifi.WifiScanner;
import android.net.wifi.WifiWakeReasonAndCounts;
+import android.os.INetworkManagementService;
+import android.os.RemoteException;
import android.os.SystemClock;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
@@ -36,10 +40,13 @@ import android.util.SparseArray;
import com.android.internal.annotations.Immutable;
import com.android.internal.util.HexDump;
import com.android.server.connectivity.KeepalivePacketData;
+import com.android.server.net.BaseNetworkObserver;
import com.android.server.wifi.util.FrameParser;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
@@ -48,12 +55,14 @@ import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
-
/**
* Native calls for bring up/shut down of the supplicant daemon and for
* sending requests to the supplicant daemon
@@ -66,14 +75,19 @@ public class WifiNative {
private final SupplicantStaIfaceHal mSupplicantStaIfaceHal;
private final WifiVendorHal mWifiVendorHal;
private final WificondControl mWificondControl;
+ private final INetworkManagementService mNwManagementService;
+ // TODO(b/69426063): Remove interfaceName from constructor once WifiStateMachine switches over
+ // to the new interface management methods.
public WifiNative(String interfaceName, WifiVendorHal vendorHal,
- SupplicantStaIfaceHal staIfaceHal, WificondControl condControl) {
+ SupplicantStaIfaceHal staIfaceHal, WificondControl condControl,
+ INetworkManagementService nwService) {
mTAG = "WifiNative-" + interfaceName;
mInterfaceName = interfaceName;
mWifiVendorHal = vendorHal;
mSupplicantStaIfaceHal = staIfaceHal;
mWificondControl = condControl;
+ mNwManagementService = nwService;
}
public String getInterfaceName() {
@@ -111,7 +125,7 @@ public class WifiNative {
Log.e(mTAG, "Failed to start HAL for client mode");
return Pair.create(SETUP_FAILURE_HAL, null);
}
- IClientInterface iClientInterface = mWificondControl.setupDriverForClientMode(ifaceName);
+ IClientInterface iClientInterface = mWificondControl.setupInterfaceForClientMode(ifaceName);
if (iClientInterface == null) {
return Pair.create(SETUP_FAILURE_WIFICOND, null);
}
@@ -132,7 +146,7 @@ public class WifiNative {
Log.e(mTAG, "Failed to start HAL for AP mode");
return Pair.create(SETUP_FAILURE_HAL, null);
}
- IApInterface iApInterface = mWificondControl.setupDriverForSoftApMode(ifaceName);
+ IApInterface iApInterface = mWificondControl.setupInterfaceForSoftApMode(ifaceName);
if (iApInterface == null) {
return Pair.create(SETUP_FAILURE_WIFICOND, null);
}
@@ -153,6 +167,574 @@ public class WifiNative {
}
}
+ /**
+ * TODO(b/69426063): NEW API Surface for interface management. This will eventually
+ * deprecate the other interface management API's above. But, for now there will be
+ * some duplication to ease transition.
+ */
+ /**
+ * Meta-info about every iface that is active.
+ */
+ private static class Iface {
+ /** Type of ifaces possible */
+ public static final int IFACE_TYPE_AP = 0;
+ public static final int IFACE_TYPE_STA = 1;
+
+ @IntDef({IFACE_TYPE_AP, IFACE_TYPE_STA})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface IfaceType{}
+
+ /** Identifier allocated for the interface */
+ public final int id;
+ /** Type of the iface: STA or AP */
+ public final @IfaceType int type;
+ /** Name of the interface */
+ public String name;
+ /** External iface destroyed listener for the iface */
+ public InterfaceCallback externalListener;
+ /** Network observer registered for this interface */
+ public NetworkObserverInternal networkObserver;
+
+ Iface(int id, @Iface.IfaceType int type) {
+ this.id = id;
+ this.type = type;
+ }
+ }
+
+ /**
+ * Iface Management entity. This class maintains list of all the active ifaces.
+ */
+ private static class IfaceManager {
+ /** Integer to allocate for the next iface being created */
+ private int mNextId;
+ /** Map of the id to the iface structure */
+ private HashMap<Integer, Iface> mIfaces = new HashMap<>();
+
+ /** Allocate a new iface for the given type */
+ private Iface allocateIface(@Iface.IfaceType int type) {
+ Iface iface = new Iface(mNextId, type);
+ mIfaces.put(mNextId, iface);
+ mNextId++;
+ return iface;
+ }
+
+ /** Remove the iface using the provided id */
+ private Iface removeIface(int id) {
+ return mIfaces.remove(id);
+ }
+
+ /** Lookup the iface using the provided id */
+ private Iface getIface(int id) {
+ return mIfaces.get(id);
+ }
+
+ /** Lookup the iface using the provided name */
+ private Iface getIface(@NonNull String ifaceName) {
+ for (Iface iface : mIfaces.values()) {
+ if (TextUtils.equals(iface.name, ifaceName)) {
+ return iface;
+ }
+ }
+ return null;
+ }
+
+ /** Iterator to use for deleting all the ifaces while performing teardown on each of them */
+ private Iterator<Integer> getIfaceIdIter() {
+ return mIfaces.keySet().iterator();
+ }
+
+ /** Checks if there are any iface active. */
+ private boolean hasAnyIface() {
+ return !mIfaces.isEmpty();
+ }
+
+ /** Checks if there are any iface of the given type active. */
+ private boolean hasAnyIfaceOfType(@Iface.IfaceType int type) {
+ for (Iface iface : mIfaces.values()) {
+ if (iface.type == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Checks if there are any STA iface active. */
+ private boolean hasAnyStaIface() {
+ return hasAnyIfaceOfType(Iface.IFACE_TYPE_STA);
+ }
+
+ /** Checks if there are any AP iface active. */
+ private boolean hasAnyApIface() {
+ return hasAnyIfaceOfType(Iface.IFACE_TYPE_AP);
+ }
+ }
+
+ private Object mLock = new Object();
+ private final IfaceManager mIfaceMgr = new IfaceManager();
+ private HashSet<StatusListener> mStatusListeners = new HashSet<>();
+
+ /** Helper method invoked to start supplicant if there were no ifaces */
+ private boolean startHal() {
+ synchronized (mLock) {
+ if (!mIfaceMgr.hasAnyIface()) {
+ if (!mWifiVendorHal.startVendorHal()) {
+ Log.e(mTAG, "Failed to start vendor HAL");
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ /** Helper method invoked to stop HAL if there are no more ifaces */
+ private void stopHalAndWificondIfNecessary() {
+ synchronized (mLock) {
+ if (!mIfaceMgr.hasAnyIface()) {
+ if (!mWificondControl.tearDownInterfaces()) {
+ Log.e(mTAG, "Failed to teardown ifaces from wificond");
+ }
+ mWifiVendorHal.stopVendorHal();
+ }
+ }
+ }
+
+ private static final int CONNECT_TO_SUPPLICANT_RETRY_INTERVAL_MS = 100;
+ private static final int CONNECT_TO_SUPPLICANT_RETRY_TIMES = 50;
+ /**
+ * This method is called to wait for establishing connection to wpa_supplicant.
+ *
+ * @return true if connection is established, false otherwise.
+ */
+ private boolean waitForSupplicantConnection() {
+ // Start initialization if not already started.
+ if (!mSupplicantStaIfaceHal.isInitializationStarted()
+ && !mSupplicantStaIfaceHal.initialize()) {
+ return false;
+ }
+ boolean connected = false;
+ int connectTries = 0;
+ while (!connected && connectTries++ < CONNECT_TO_SUPPLICANT_RETRY_TIMES) {
+ // Check if the initialization is complete.
+ connected = mSupplicantStaIfaceHal.isInitializationComplete();
+ if (connected) {
+ break;
+ }
+ try {
+ Thread.sleep(CONNECT_TO_SUPPLICANT_RETRY_INTERVAL_MS);
+ } catch (InterruptedException ignore) {
+ }
+ }
+ return connected;
+ }
+
+ /** Helper method invoked to start supplicant if there were no STA ifaces */
+ private boolean startSupplicant() {
+ synchronized (mLock) {
+ if (!mIfaceMgr.hasAnyStaIface()) {
+ if (!mWificondControl.enableSupplicant()) {
+ Log.e(mTAG, "Failed to enable supplicant");
+ return false;
+ }
+ if (!waitForSupplicantConnection()) {
+ Log.e(mTAG, "Failed to connect to supplicant");
+ return false;
+ }
+ if (!mSupplicantStaIfaceHal.registerDeathHandler(new DeathHandlerInternal())) {
+ Log.e(mTAG, "Failed to register supplicant death handler");
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ /** Helper method invoked to stop supplicant if there are no more STA ifaces */
+ private void stopSupplicantIfNecessary() {
+ synchronized (mLock) {
+ if (!mIfaceMgr.hasAnyStaIface()) {
+ if (!mSupplicantStaIfaceHal.deregisterDeathHandler()) {
+ Log.e(mTAG, "Failed to deregister supplicant death handler");
+ }
+ if (!mWificondControl.disableSupplicant()) {
+ Log.e(mTAG, "Failed to disable supplicant");
+ }
+ }
+ }
+ }
+
+ /** Helper method to register a network observer and return it */
+ private boolean registerNetworkObserver(@NonNull NetworkObserverInternal observer) {
+ try {
+ mNwManagementService.registerObserver(observer);
+ } catch (RemoteException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /** Helper method to register a network observer and return it */
+ private boolean unregisterNetworkObserver(@NonNull NetworkObserverInternal observer) {
+ try {
+ mNwManagementService.unregisterObserver(observer);
+ } catch (RemoteException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /** Helper method invoked to teardown client iface and perform necessary cleanup */
+ private void onClientInterfaceDestroyed(@NonNull Iface iface) {
+ synchronized (mLock) {
+ if (!unregisterNetworkObserver(iface.networkObserver)) {
+ Log.e(mTAG, "Failed to unregister network observer for iface=" + iface.name);
+ }
+ if (!mSupplicantStaIfaceHal.teardownIface(iface.name)) {
+ Log.e(mTAG, "Failed to teardown iface in supplicant=" + iface.name);
+ }
+ if (!mWificondControl.tearDownClientInterface(iface.name)) {
+ Log.e(mTAG, "Failed to teardown iface in wificond=" + iface.name);
+ }
+ stopSupplicantIfNecessary();
+ stopHalAndWificondIfNecessary();
+ }
+ }
+
+ /** Helper method invoked to teardown softAp iface and perform necessary cleanup */
+ private void onSoftApInterfaceDestroyed(@NonNull Iface iface) {
+ synchronized (mLock) {
+ if (!unregisterNetworkObserver(iface.networkObserver)) {
+ Log.e(mTAG, "Failed to unregister network observer for iface=" + iface.name);
+ }
+ if (!mWificondControl.stopSoftAp(iface.name)) {
+ Log.e(mTAG, "Failed to stop softap on iface=" + iface.name);
+ }
+ if (!mWificondControl.tearDownSoftApInterface(iface.name)) {
+ Log.e(mTAG, "Failed to teardown iface in wificond=" + iface.name);
+ }
+ stopHalAndWificondIfNecessary();
+ }
+ }
+
+ /** Helper method invoked to teardown iface and perform necessary cleanup */
+ private void onInterfaceDestroyed(@NonNull Iface iface) {
+ synchronized (mLock) {
+ if (iface.type == Iface.IFACE_TYPE_STA) {
+ onClientInterfaceDestroyed(iface);
+ } else if (iface.type == Iface.IFACE_TYPE_AP) {
+ onSoftApInterfaceDestroyed(iface);
+ }
+ // Invoke the external callback.
+ iface.externalListener.onDestroyed(iface.name);
+ }
+ }
+
+ /**
+ * Callback to be invoked by HalDeviceManager when an interface is destroyed.
+ */
+ private class InterfaceDestoyedListenerInternal
+ implements HalDeviceManager.InterfaceDestroyedListener {
+ /** Identifier allocated for the interface */
+ private final int mInterfaceId;
+
+ InterfaceDestoyedListenerInternal(int ifaceId) {
+ mInterfaceId = ifaceId;
+ }
+
+ @Override
+ public void onDestroyed(@NonNull String ifaceName) {
+ synchronized (mLock) {
+ final Iface iface = mIfaceMgr.removeIface(mInterfaceId);
+ if (iface == null) {
+ Log.e(mTAG, "Received iface destroyed notification on an invalid iface="
+ + ifaceName);
+ return;
+ }
+ onInterfaceDestroyed(iface);
+ Log.i(mTAG, "Successfully torn down iface=" + ifaceName);
+ }
+ }
+ }
+
+ /**
+ * Common death handler for any of the lower layer daemons.
+ */
+ private class DeathHandlerInternal implements VendorHalDeathEventHandler,
+ SupplicantDeathEventHandler, WificondDeathEventHandler {
+ @Override
+ public void onDeath() {
+ synchronized (mLock) {
+ Log.i(mTAG, "One of the daemons died. Tearing down everything");
+ Iterator<Integer> ifaceIdIter = mIfaceMgr.getIfaceIdIter();
+ while (ifaceIdIter.hasNext()) {
+ Iface iface = mIfaceMgr.getIface(ifaceIdIter.next());
+ ifaceIdIter.remove();
+ onInterfaceDestroyed(iface);
+ Log.i(mTAG, "Successfully torn down iface=" + iface.name);
+ }
+ for (StatusListener listener : mStatusListeners) {
+ listener.onStatusChanged(false);
+ }
+ // TODO(70572148): Do we need to wait to mark the system ready again?
+ for (StatusListener listener : mStatusListeners) {
+ listener.onStatusChanged(true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Network observer to use for all interface up/down notifications.
+ */
+ private class NetworkObserverInternal extends BaseNetworkObserver {
+ /** Identifier allocated for the interface */
+ private final int mInterfaceId;
+
+ NetworkObserverInternal(int id) {
+ mInterfaceId = id;
+ }
+
+ @Override
+ public void interfaceLinkStateChanged(String ifaceName, boolean isUp) {
+ synchronized (mLock) {
+ Log.i(mTAG, "Interface link state changed=" + ifaceName + ", isUp=" + isUp);
+ final Iface iface = mIfaceMgr.getIface(mInterfaceId);
+ if (iface == null) {
+ Log.e(mTAG, "Received iface up/down notification on an invalid iface="
+ + ifaceName);
+ return;
+ }
+ if (isUp) {
+ iface.externalListener.onUp(ifaceName);
+ } else {
+ iface.externalListener.onDown(ifaceName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Initialize the native modules.
+ *
+ * @return true on success, false otherwise.
+ */
+ public boolean initialize() {
+ synchronized (mLock) {
+ if (!mWifiVendorHal.initialize(new DeathHandlerInternal())) {
+ Log.e(mTAG, "Failed to initialize vendor HAL");
+ return false;
+ }
+ if (!mWificondControl.registerDeathHandler(new DeathHandlerInternal())) {
+ Log.e(mTAG, "Failed to initialize wificond");
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Callback to notify when the status of one of the native daemons
+ * (wificond, wpa_supplicant & vendor HAL) changes.
+ */
+ public interface StatusListener {
+ /**
+ * @param allReady Indicates if all the native daemons are ready for operation or not.
+ */
+ void onStatusChanged(boolean allReady);
+ }
+
+ /**
+ * Register a StatusListener to get notified about any status changes from the native daemons.
+ *
+ * It is safe to re-register the same callback object - duplicates are detected and only a
+ * single copy kept.
+ *
+ * @param listener StatusListener listener object.
+ */
+ public void registerStatusListener(@NonNull StatusListener listener) {
+ mStatusListeners.add(listener);
+ }
+
+ /**
+ * Callback to notify when the associated interface is destroyed, up or down.
+ */
+ public interface InterfaceCallback {
+ /**
+ * Interface destroyed by HalDeviceManager.
+ *
+ * @param ifaceName Name of the iface.
+ */
+ void onDestroyed(String ifaceName);
+
+ /**
+ * Interface is up.
+ *
+ * @param ifaceName Name of the iface.
+ */
+ void onUp(String ifaceName);
+
+ /**
+ * Interface is down.
+ *
+ * @param ifaceName Name of the iface.
+ */
+ void onDown(String ifaceName);
+ }
+
+ /**
+ * Setup an interface for Client mode operations.
+ *
+ * This method configures an interface in STA mode in all the native daemons
+ * (wificond, wpa_supplicant & vendor HAL).
+ *
+ * @param interfaceCallback Associated callback for notifying status changes for the iface.
+ * @return Returns the name of the allocated interface, will be null on failure.
+ */
+ public String setupInterfaceForClientMode(@NonNull InterfaceCallback interfaceCallback) {
+ synchronized (mLock) {
+ if (!startHal()) {
+ Log.e(mTAG, "Failed to start Hal");
+ return null;
+ }
+ if (!startSupplicant()) {
+ Log.e(mTAG, "Failed to start supplicant");
+ return null;
+ }
+ Iface iface = mIfaceMgr.allocateIface(Iface.IFACE_TYPE_STA);
+ if (iface == null) {
+ Log.e(mTAG, "Failed to allocate new STA iface");
+ return null;
+ }
+ iface.externalListener = interfaceCallback;
+ iface.name =
+ mWifiVendorHal.createStaIface(new InterfaceDestoyedListenerInternal(iface.id));
+ if (TextUtils.isEmpty(iface.name)) {
+ Log.e(mTAG, "Failed to create iface in vendor HAL");
+ mIfaceMgr.removeIface(iface.id);
+ return null;
+ }
+ if (mWificondControl.setupInterfaceForClientMode(iface.name) == null) {
+ Log.e(mTAG, "Failed to setup iface in wificond=" + iface.name);
+ teardownInterface(iface.name);
+ return null;
+ }
+ if (!mSupplicantStaIfaceHal.setupIface(iface.name)) {
+ Log.e(mTAG, "Failed to setup iface in supplicant=" + iface.name);
+ teardownInterface(iface.name);
+ return null;
+ }
+ iface.networkObserver = new NetworkObserverInternal(iface.id);
+ if (!registerNetworkObserver(iface.networkObserver)) {
+ Log.e(mTAG, "Failed to register network observer for iface=" + iface.name);
+ teardownInterface(iface.name);
+ return null;
+ }
+ Log.i(mTAG, "Successfully setup iface=" + iface.name);
+ return iface.name;
+ }
+ }
+
+ /**
+ * Setup an interface for Soft AP mode operations.
+ *
+ * This method configures an interface in AP mode in all the native daemons
+ * (wificond, wpa_supplicant & vendor HAL).
+ *
+ * @param interfaceCallback Associated callback for notifying status changes for the iface.
+ * @return Returns the name of the allocated interface, will be null on failure.
+ */
+ public String setupInterfaceForSoftApMode(@NonNull InterfaceCallback interfaceCallback) {
+ synchronized (mLock) {
+ if (!startHal()) {
+ Log.e(mTAG, "Failed to start Hal");
+ return null;
+ }
+ Iface iface = mIfaceMgr.allocateIface(Iface.IFACE_TYPE_AP);
+ if (iface == null) {
+ Log.e(mTAG, "Failed to allocate new AP iface");
+ return null;
+ }
+ iface.externalListener = interfaceCallback;
+ iface.name =
+ mWifiVendorHal.createApIface(new InterfaceDestoyedListenerInternal(iface.id));
+ if (TextUtils.isEmpty(iface.name)) {
+ Log.e(mTAG, "Failed to create iface in vendor HAL");
+ mIfaceMgr.removeIface(iface.id);
+ return null;
+ }
+ if (mWificondControl.setupInterfaceForSoftApMode(iface.name) == null) {
+ Log.e(mTAG, "Failed to setup iface in wificond=" + iface.name);
+ teardownInterface(iface.name);
+ return null;
+ }
+ iface.networkObserver = new NetworkObserverInternal(iface.id);
+ if (!registerNetworkObserver(iface.networkObserver)) {
+ Log.e(mTAG, "Failed to register network observer for iface=" + iface.name);
+ teardownInterface(iface.name);
+ return null;
+ }
+ Log.i(mTAG, "Successfully setup iface=" + iface.name);
+ return iface.name;
+ }
+ }
+
+ /**
+ * Check if the interface is up or down.
+ *
+ * @param ifaceName Name of the interface.
+ * @return true if iface is up, false if it's down or on error.
+ */
+ public boolean isInterfaceUp(@NonNull String ifaceName) {
+ synchronized (mLock) {
+ final Iface iface = mIfaceMgr.getIface(ifaceName);
+ if (iface == null) {
+ Log.e(mTAG, "Trying to get iface state on invalid iface=" + ifaceName);
+ return false;
+ }
+ InterfaceConfiguration config = null;
+ try {
+ config = mNwManagementService.getInterfaceConfig(ifaceName);
+ } catch (RemoteException e) {
+ }
+ if (config == null) {
+ return false;
+ }
+ return config.isUp();
+ }
+ }
+
+ /**
+ * Teardown an interface in Client/AP mode.
+ *
+ * This method tears down the associated interface from all the native daemons
+ * (wificond, wpa_supplicant & vendor HAL).
+ *
+ * @param ifaceName Name of the interface.
+ */
+ public void teardownInterface(@NonNull String ifaceName) {
+ synchronized (mLock) {
+ final Iface iface = mIfaceMgr.getIface(ifaceName);
+ if (iface == null) {
+ Log.e(mTAG, "Trying to teardown an invalid iface=" + ifaceName);
+ return;
+ }
+ // Trigger the iface removal from HAL. The rest of the cleanup will be triggered
+ // from the interface destroyed callback.
+ // TODO(b/70521011): Figure out what to do for devices with no HAL.
+ if (iface.type == Iface.IFACE_TYPE_STA) {
+ if (!mWifiVendorHal.removeStaIface(ifaceName)) {
+ Log.e(mTAG, "Failed to remove iface in vendor HAL=" + ifaceName);
+ return;
+ }
+ } else if (iface.type == Iface.IFACE_TYPE_AP) {
+ if (!mWifiVendorHal.removeApIface(ifaceName)) {
+ Log.e(mTAG, "Failed to remove iface in vendor HAL=" + ifaceName);
+ return;
+ }
+ }
+ Log.i(mTAG, "Successfully initiated teardown for iface=" + ifaceName);
+ }
+ }
+
/********************************************************
* Wificond operations
********************************************************/
@@ -179,6 +761,32 @@ public class WifiNative {
}
/**
+ * Callback to notify wificond death.
+ */
+ public interface WificondDeathEventHandler {
+ /**
+ * Invoked when the wificond dies.
+ */
+ void onDeath();
+ }
+
+ /**
+ * Registers a death notification for wificond.
+ * @return Returns true on success.
+ */
+ public boolean registerWificondDeathHandler(@NonNull WificondDeathEventHandler handler) {
+ return mWificondControl.registerDeathHandler(handler);
+ }
+
+ /**
+ * Deregisters a death notification for wificond.
+ * @return Returns true on success.
+ */
+ public boolean deregisterWificondDeathHandler() {
+ return mWificondControl.deregisterDeathHandler();
+ }
+
+ /**
* Disable wpa_supplicant via wificond.
* @return Returns true on success.
*/
@@ -200,7 +808,7 @@ public class WifiNative {
* Returns null on failure.
*/
public SignalPollResult signalPoll() {
- return mWificondControl.signalPoll();
+ return mWificondControl.signalPoll(mInterfaceName);
}
/**
@@ -209,7 +817,7 @@ public class WifiNative {
* Returns null on failure.
*/
public TxPacketCounters getTxPacketCounters() {
- return mWificondControl.getTxPacketCounters();
+ return mWificondControl.getTxPacketCounters(mInterfaceName);
}
/**
@@ -235,7 +843,7 @@ public class WifiNative {
* @return Returns true on success.
*/
public boolean scan(Set<Integer> freqs, Set<String> hiddenNetworkSSIDs) {
- return mWificondControl.scan(freqs, hiddenNetworkSSIDs);
+ return mWificondControl.scan(mInterfaceName, freqs, hiddenNetworkSSIDs);
}
/**
@@ -244,7 +852,8 @@ public class WifiNative {
* Returns an empty ArrayList on failure.
*/
public ArrayList<ScanDetail> getScanResults() {
- return mWificondControl.getScanResults(WificondControl.SCAN_TYPE_SINGLE_SCAN);
+ return mWificondControl.getScanResults(
+ mInterfaceName, WificondControl.SCAN_TYPE_SINGLE_SCAN);
}
/**
@@ -253,7 +862,7 @@ public class WifiNative {
* Returns an empty ArrayList on failure.
*/
public ArrayList<ScanDetail> getPnoScanResults() {
- return mWificondControl.getScanResults(WificondControl.SCAN_TYPE_PNO_SCAN);
+ return mWificondControl.getScanResults(mInterfaceName, WificondControl.SCAN_TYPE_PNO_SCAN);
}
/**
@@ -262,7 +871,7 @@ public class WifiNative {
* @return true on success.
*/
public boolean startPnoScan(PnoSettings pnoSettings) {
- return mWificondControl.startPnoScan(pnoSettings);
+ return mWificondControl.startPnoScan(mInterfaceName, pnoSettings);
}
/**
@@ -270,7 +879,7 @@ public class WifiNative {
* @return true on success.
*/
public boolean stopPnoScan() {
- return mWificondControl.stopPnoScan();
+ return mWificondControl.stopPnoScan(mInterfaceName);
}
/**
@@ -291,7 +900,7 @@ public class WifiNative {
* @return true on success, false otherwise.
*/
public boolean startSoftAp(WifiConfiguration config, SoftApListener listener) {
- return mWificondControl.startSoftAp(config, listener);
+ return mWificondControl.startSoftAp(mInterfaceName, config, listener);
}
/**
@@ -300,7 +909,7 @@ public class WifiNative {
* @return true on success, false otherwise.
*/
public boolean stopSoftAp() {
- return mWificondControl.stopSoftAp();
+ return mWificondControl.stopSoftAp(mInterfaceName);
}
/********************************************************
@@ -308,7 +917,26 @@ public class WifiNative {
********************************************************/
/**
- * This method is called repeatedly until the connection to wpa_supplicant is established.
+ * Callback to notify supplicant death.
+ */
+ public interface SupplicantDeathEventHandler {
+ /**
+ * Invoked when the supplicant dies.
+ */
+ void onDeath();
+ }
+
+ /**
+ * Registers a death notification for supplicant.
+ * @return Returns true on success.
+ */
+ public boolean registerSupplicantDeathHandler(@NonNull SupplicantDeathEventHandler handler) {
+ return mSupplicantStaIfaceHal.registerDeathHandler(handler);
+ }
+
+ /**
+ * This method is called repeatedly until the connection to wpa_supplicant is
+ * established and a STA iface is setup.
*
* @return true if connection is established, false otherwise.
* TODO: Add unit tests for these once we remove the legacy code.
@@ -320,14 +948,17 @@ public class WifiNative {
return false;
}
// Check if the initialization is complete.
- return mSupplicantStaIfaceHal.isInitializationComplete();
+ if (!mSupplicantStaIfaceHal.isInitializationComplete()) {
+ return false;
+ }
+ // Setup the STA iface once connection is established.
+ return mSupplicantStaIfaceHal.setupIface(mInterfaceName);
}
/**
* Close supplicant connection.
*/
public void closeSupplicantConnection() {
- // Nothing to do for HIDL.
}
/**
@@ -345,7 +976,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean reconnect() {
- return mSupplicantStaIfaceHal.reconnect();
+ return mSupplicantStaIfaceHal.reconnect(mInterfaceName);
}
/**
@@ -354,7 +985,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean reassociate() {
- return mSupplicantStaIfaceHal.reassociate();
+ return mSupplicantStaIfaceHal.reassociate(mInterfaceName);
}
/**
@@ -363,7 +994,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean disconnect() {
- return mSupplicantStaIfaceHal.disconnect();
+ return mSupplicantStaIfaceHal.disconnect(mInterfaceName);
}
/**
@@ -372,7 +1003,7 @@ public class WifiNative {
* @return string containing the MAC address, or null on a failed call
*/
public String getMacAddress() {
- return mSupplicantStaIfaceHal.getMacAddress();
+ return mSupplicantStaIfaceHal.getMacAddress(mInterfaceName);
}
public static final int RX_FILTER_TYPE_V4_MULTICAST = 0;
@@ -402,10 +1033,10 @@ public class WifiNative {
* The SETSUSPENDOPT driver command overrides the filtering rules
*/
public boolean startFilteringMulticastV4Packets() {
- return mSupplicantStaIfaceHal.stopRxFilter()
+ return mSupplicantStaIfaceHal.stopRxFilter(mInterfaceName)
&& mSupplicantStaIfaceHal.removeRxFilter(
- RX_FILTER_TYPE_V4_MULTICAST)
- && mSupplicantStaIfaceHal.startRxFilter();
+ mInterfaceName, RX_FILTER_TYPE_V4_MULTICAST)
+ && mSupplicantStaIfaceHal.startRxFilter(mInterfaceName);
}
/**
@@ -413,10 +1044,10 @@ public class WifiNative {
* @return {@code true} if the operation succeeded, {@code false} otherwise
*/
public boolean stopFilteringMulticastV4Packets() {
- return mSupplicantStaIfaceHal.stopRxFilter()
+ return mSupplicantStaIfaceHal.stopRxFilter(mInterfaceName)
&& mSupplicantStaIfaceHal.addRxFilter(
- RX_FILTER_TYPE_V4_MULTICAST)
- && mSupplicantStaIfaceHal.startRxFilter();
+ mInterfaceName, RX_FILTER_TYPE_V4_MULTICAST)
+ && mSupplicantStaIfaceHal.startRxFilter(mInterfaceName);
}
/**
@@ -424,10 +1055,10 @@ public class WifiNative {
* @return {@code true} if the operation succeeded, {@code false} otherwise
*/
public boolean startFilteringMulticastV6Packets() {
- return mSupplicantStaIfaceHal.stopRxFilter()
+ return mSupplicantStaIfaceHal.stopRxFilter(mInterfaceName)
&& mSupplicantStaIfaceHal.removeRxFilter(
- RX_FILTER_TYPE_V6_MULTICAST)
- && mSupplicantStaIfaceHal.startRxFilter();
+ mInterfaceName, RX_FILTER_TYPE_V6_MULTICAST)
+ && mSupplicantStaIfaceHal.startRxFilter(mInterfaceName);
}
/**
@@ -435,10 +1066,10 @@ public class WifiNative {
* @return {@code true} if the operation succeeded, {@code false} otherwise
*/
public boolean stopFilteringMulticastV6Packets() {
- return mSupplicantStaIfaceHal.stopRxFilter()
+ return mSupplicantStaIfaceHal.stopRxFilter(mInterfaceName)
&& mSupplicantStaIfaceHal.addRxFilter(
- RX_FILTER_TYPE_V6_MULTICAST)
- && mSupplicantStaIfaceHal.startRxFilter();
+ mInterfaceName, RX_FILTER_TYPE_V6_MULTICAST)
+ && mSupplicantStaIfaceHal.startRxFilter(mInterfaceName);
}
public static final int BLUETOOTH_COEXISTENCE_MODE_ENABLED = 0;
@@ -453,7 +1084,7 @@ public class WifiNative {
* @return Whether the mode was successfully set.
*/
public boolean setBluetoothCoexistenceMode(int mode) {
- return mSupplicantStaIfaceHal.setBtCoexistenceMode(mode);
+ return mSupplicantStaIfaceHal.setBtCoexistenceMode(mInterfaceName, mode);
}
/**
@@ -465,7 +1096,8 @@ public class WifiNative {
* @return {@code true} if the command succeeded, {@code false} otherwise.
*/
public boolean setBluetoothCoexistenceScanMode(boolean setCoexScanMode) {
- return mSupplicantStaIfaceHal.setBtCoexistenceScanModeEnabled(setCoexScanMode);
+ return mSupplicantStaIfaceHal.setBtCoexistenceScanModeEnabled(
+ mInterfaceName, setCoexScanMode);
}
/**
@@ -475,7 +1107,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setSuspendOptimizations(boolean enabled) {
- return mSupplicantStaIfaceHal.setSuspendModeEnabled(enabled);
+ return mSupplicantStaIfaceHal.setSuspendModeEnabled(mInterfaceName, enabled);
}
/**
@@ -485,7 +1117,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setCountryCode(String countryCode) {
- return mSupplicantStaIfaceHal.setCountryCode(countryCode);
+ return mSupplicantStaIfaceHal.setCountryCode(mInterfaceName, countryCode);
}
/**
@@ -496,10 +1128,10 @@ public class WifiNative {
*/
public void startTdls(String macAddr, boolean enable) {
if (enable) {
- mSupplicantStaIfaceHal.initiateTdlsDiscover(macAddr);
- mSupplicantStaIfaceHal.initiateTdlsSetup(macAddr);
+ mSupplicantStaIfaceHal.initiateTdlsDiscover(mInterfaceName, macAddr);
+ mSupplicantStaIfaceHal.initiateTdlsSetup(mInterfaceName, macAddr);
} else {
- mSupplicantStaIfaceHal.initiateTdlsTeardown(macAddr);
+ mSupplicantStaIfaceHal.initiateTdlsTeardown(mInterfaceName, macAddr);
}
}
@@ -510,7 +1142,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean startWpsPbc(String bssid) {
- return mSupplicantStaIfaceHal.startWpsPbc(bssid);
+ return mSupplicantStaIfaceHal.startWpsPbc(mInterfaceName, bssid);
}
/**
@@ -520,7 +1152,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean startWpsPinKeypad(String pin) {
- return mSupplicantStaIfaceHal.startWpsPinKeypad(pin);
+ return mSupplicantStaIfaceHal.startWpsPinKeypad(mInterfaceName, pin);
}
/**
@@ -530,7 +1162,7 @@ public class WifiNative {
* @return new pin generated on success, null otherwise.
*/
public String startWpsPinDisplay(String bssid) {
- return mSupplicantStaIfaceHal.startWpsPinDisplay(bssid);
+ return mSupplicantStaIfaceHal.startWpsPinDisplay(mInterfaceName, bssid);
}
/**
@@ -540,7 +1172,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setExternalSim(boolean external) {
- return mSupplicantStaIfaceHal.setExternalSim(external);
+ return mSupplicantStaIfaceHal.setExternalSim(mInterfaceName, external);
}
/**
@@ -559,11 +1191,14 @@ public class WifiNative {
*/
public boolean simAuthResponse(int id, String type, String response) {
if (SIM_AUTH_RESP_TYPE_GSM_AUTH.equals(type)) {
- return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimGsmAuthResponse(response);
+ return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimGsmAuthResponse(
+ mInterfaceName, response);
} else if (SIM_AUTH_RESP_TYPE_UMTS_AUTH.equals(type)) {
- return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAuthResponse(response);
+ return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAuthResponse(
+ mInterfaceName, response);
} else if (SIM_AUTH_RESP_TYPE_UMTS_AUTS.equals(type)) {
- return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAutsResponse(response);
+ return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAutsResponse(
+ mInterfaceName, response);
} else {
return false;
}
@@ -575,7 +1210,7 @@ public class WifiNative {
* @return true if succeeds, false otherwise.
*/
public boolean simAuthFailedResponse(int id) {
- return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimGsmAuthFailure();
+ return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimGsmAuthFailure(mInterfaceName);
}
/**
@@ -584,7 +1219,7 @@ public class WifiNative {
* @return true if succeeds, false otherwise.
*/
public boolean umtsAuthFailedResponse(int id) {
- return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAuthFailure();
+ return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAuthFailure(mInterfaceName);
}
/**
@@ -594,7 +1229,8 @@ public class WifiNative {
* @return true if succeeds, false otherwise.
*/
public boolean simIdentityResponse(int id, String response) {
- return mSupplicantStaIfaceHal.sendCurrentNetworkEapIdentityResponse(response);
+ return mSupplicantStaIfaceHal.sendCurrentNetworkEapIdentityResponse(
+ mInterfaceName, response);
}
/**
@@ -603,7 +1239,7 @@ public class WifiNative {
* @return anonymous identity string if succeeds, null otherwise.
*/
public String getEapAnonymousIdentity() {
- return mSupplicantStaIfaceHal.getCurrentNetworkEapAnonymousIdentity();
+ return mSupplicantStaIfaceHal.getCurrentNetworkEapAnonymousIdentity(mInterfaceName);
}
/**
@@ -614,7 +1250,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean startWpsRegistrar(String bssid, String pin) {
- return mSupplicantStaIfaceHal.startWpsRegistrar(bssid, pin);
+ return mSupplicantStaIfaceHal.startWpsRegistrar(mInterfaceName, bssid, pin);
}
/**
@@ -623,7 +1259,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean cancelWps() {
- return mSupplicantStaIfaceHal.cancelWps();
+ return mSupplicantStaIfaceHal.cancelWps(mInterfaceName);
}
/**
@@ -633,7 +1269,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setDeviceName(String name) {
- return mSupplicantStaIfaceHal.setWpsDeviceName(name);
+ return mSupplicantStaIfaceHal.setWpsDeviceName(mInterfaceName, name);
}
/**
@@ -643,7 +1279,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setDeviceType(String type) {
- return mSupplicantStaIfaceHal.setWpsDeviceType(type);
+ return mSupplicantStaIfaceHal.setWpsDeviceType(mInterfaceName, type);
}
/**
@@ -653,7 +1289,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setConfigMethods(String cfg) {
- return mSupplicantStaIfaceHal.setWpsConfigMethods(cfg);
+ return mSupplicantStaIfaceHal.setWpsConfigMethods(mInterfaceName, cfg);
}
/**
@@ -663,7 +1299,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setManufacturer(String value) {
- return mSupplicantStaIfaceHal.setWpsManufacturer(value);
+ return mSupplicantStaIfaceHal.setWpsManufacturer(mInterfaceName, value);
}
/**
@@ -673,7 +1309,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setModelName(String value) {
- return mSupplicantStaIfaceHal.setWpsModelName(value);
+ return mSupplicantStaIfaceHal.setWpsModelName(mInterfaceName, value);
}
/**
@@ -683,7 +1319,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setModelNumber(String value) {
- return mSupplicantStaIfaceHal.setWpsModelNumber(value);
+ return mSupplicantStaIfaceHal.setWpsModelNumber(mInterfaceName, value);
}
/**
@@ -693,7 +1329,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean setSerialNumber(String value) {
- return mSupplicantStaIfaceHal.setWpsSerialNumber(value);
+ return mSupplicantStaIfaceHal.setWpsSerialNumber(mInterfaceName, value);
}
/**
@@ -702,7 +1338,7 @@ public class WifiNative {
* @param enabled true to enable, false to disable.
*/
public void setPowerSave(boolean enabled) {
- mSupplicantStaIfaceHal.setPowerSave(enabled);
+ mSupplicantStaIfaceHal.setPowerSave(mInterfaceName, enabled);
}
/**
@@ -723,7 +1359,7 @@ public class WifiNative {
* @return true if request is sent successfully, false otherwise.
*/
public boolean enableStaAutoReconnect(boolean enable) {
- return mSupplicantStaIfaceHal.enableAutoReconnect(enable);
+ return mSupplicantStaIfaceHal.enableAutoReconnect(mInterfaceName, enable);
}
/**
@@ -736,7 +1372,7 @@ public class WifiNative {
*/
public boolean migrateNetworksFromSupplicant(Map<String, WifiConfiguration> configs,
SparseArray<Map<String, String>> networkExtras) {
- return mSupplicantStaIfaceHal.loadNetworks(configs, networkExtras);
+ return mSupplicantStaIfaceHal.loadNetworks(mInterfaceName, configs, networkExtras);
}
/**
@@ -754,8 +1390,8 @@ public class WifiNative {
*/
public boolean connectToNetwork(WifiConfiguration configuration) {
// Abort ongoing scan before connect() to unblock connection request.
- mWificondControl.abortScan();
- return mSupplicantStaIfaceHal.connectToNetwork(configuration);
+ mWificondControl.abortScan(mInterfaceName);
+ return mSupplicantStaIfaceHal.connectToNetwork(mInterfaceName, configuration);
}
/**
@@ -773,8 +1409,8 @@ public class WifiNative {
*/
public boolean roamToNetwork(WifiConfiguration configuration) {
// Abort ongoing scan before connect() to unblock roaming request.
- mWificondControl.abortScan();
- return mSupplicantStaIfaceHal.roamToNetwork(configuration);
+ mWificondControl.abortScan(mInterfaceName);
+ return mSupplicantStaIfaceHal.roamToNetwork(mInterfaceName, configuration);
}
/**
@@ -794,7 +1430,7 @@ public class WifiNative {
* @return {@code true} if it succeeds, {@code false} otherwise
*/
public boolean removeAllNetworks() {
- return mSupplicantStaIfaceHal.removeAllNetworks();
+ return mSupplicantStaIfaceHal.removeAllNetworks(mInterfaceName);
}
/**
@@ -803,7 +1439,7 @@ public class WifiNative {
* @return true if successful, false otherwise.
*/
public boolean setConfiguredNetworkBSSID(String bssid) {
- return mSupplicantStaIfaceHal.setCurrentNetworkBssid(bssid);
+ return mSupplicantStaIfaceHal.setCurrentNetworkBssid(mInterfaceName, bssid);
}
/**
@@ -826,7 +1462,8 @@ public class WifiNative {
}
ArrayList<Integer> hs20SubtypeList = new ArrayList<>();
hs20SubtypeList.addAll(hs20Subtypes);
- return mSupplicantStaIfaceHal.initiateAnqpQuery(bssid, anqpIdList, hs20SubtypeList);
+ return mSupplicantStaIfaceHal.initiateAnqpQuery(
+ mInterfaceName, bssid, anqpIdList, hs20SubtypeList);
}
/**
@@ -840,7 +1477,7 @@ public class WifiNative {
Log.e(mTAG, "Invalid arguments for Icon request.");
return false;
}
- return mSupplicantStaIfaceHal.initiateHs20IconQuery(bssid, fileName);
+ return mSupplicantStaIfaceHal.initiateHs20IconQuery(mInterfaceName, bssid, fileName);
}
/**
@@ -849,7 +1486,7 @@ public class WifiNative {
* @return Hex string corresponding to the WPS NFC token.
*/
public String getCurrentNetworkWpsNfcConfigurationToken() {
- return mSupplicantStaIfaceHal.getCurrentNetworkWpsNfcConfigurationToken();
+ return mSupplicantStaIfaceHal.getCurrentNetworkWpsNfcConfigurationToken(mInterfaceName);
}
/** Remove the request |networkId| from supplicant if it's the current network,
@@ -858,7 +1495,7 @@ public class WifiNative {
* @param networkId network id of the network to be removed from supplicant.
*/
public void removeNetworkIfCurrent(int networkId) {
- mSupplicantStaIfaceHal.removeNetworkIfCurrent(networkId);
+ mSupplicantStaIfaceHal.removeNetworkIfCurrent(mInterfaceName, networkId);
}
/********************************************************
@@ -892,7 +1529,11 @@ public class WifiNative {
Log.i(mTAG, "Vendor HAL not supported, Ignore start...");
return true;
}
- return mWifiVendorHal.startVendorHal(isStaMode);
+ if (isStaMode) {
+ return mWifiVendorHal.startVendorHalSta();
+ } else {
+ return mWifiVendorHal.startVendorHalAp();
+ }
}
/**
@@ -929,7 +1570,7 @@ public class WifiNative {
* @return true for success. false for failure
*/
public boolean getBgScanCapabilities(ScanCapabilities capabilities) {
- return mWifiVendorHal.getBgScanCapabilities(capabilities);
+ return mWifiVendorHal.getBgScanCapabilities(mInterfaceName, capabilities);
}
public static class ChannelSettings {
@@ -1079,39 +1720,39 @@ public class WifiNative {
* @return true for success
*/
public boolean startBgScan(ScanSettings settings, ScanEventHandler eventHandler) {
- return mWifiVendorHal.startBgScan(settings, eventHandler);
+ return mWifiVendorHal.startBgScan(mInterfaceName, settings, eventHandler);
}
/**
* Stops any ongoing backgound scan
*/
public void stopBgScan() {
- mWifiVendorHal.stopBgScan();
+ mWifiVendorHal.stopBgScan(mInterfaceName);
}
/**
* Pauses an ongoing backgound scan
*/
public void pauseBgScan() {
- mWifiVendorHal.pauseBgScan();
+ mWifiVendorHal.pauseBgScan(mInterfaceName);
}
/**
* Restarts a paused scan
*/
public void restartBgScan() {
- mWifiVendorHal.restartBgScan();
+ mWifiVendorHal.restartBgScan(mInterfaceName);
}
/**
* Gets the latest scan results received.
*/
public WifiScanner.ScanData[] getBgScanResults() {
- return mWifiVendorHal.getBgScanResults();
+ return mWifiVendorHal.getBgScanResults(mInterfaceName);
}
- public WifiLinkLayerStats getWifiLinkLayerStats(String iface) {
- return mWifiVendorHal.getWifiLinkLayerStats();
+ public WifiLinkLayerStats getWifiLinkLayerStats() {
+ return mWifiVendorHal.getWifiLinkLayerStats(mInterfaceName);
}
/**
@@ -1120,7 +1761,7 @@ public class WifiNative {
* @return bitmask defined by WifiManager.WIFI_FEATURE_*
*/
public int getSupportedFeatureSet() {
- return mWifiVendorHal.getSupportedFeatureSet();
+ return mWifiVendorHal.getSupportedFeatureSet(mInterfaceName);
}
public static interface RttEventHandler {
@@ -1177,7 +1818,7 @@ public class WifiNative {
* @return true for success
*/
public boolean setScanningMacOui(byte[] oui) {
- return mWifiVendorHal.setScanningMacOui(oui);
+ return mWifiVendorHal.setScanningMacOui(mInterfaceName, oui);
}
/**
@@ -1191,7 +1832,7 @@ public class WifiNative {
* Get the APF (Android Packet Filter) capabilities of the device
*/
public ApfCapabilities getApfCapabilities() {
- return mWifiVendorHal.getApfCapabilities();
+ return mWifiVendorHal.getApfCapabilities(mInterfaceName);
}
/**
@@ -1201,7 +1842,7 @@ public class WifiNative {
* @return true for success
*/
public boolean installPacketFilter(byte[] filter) {
- return mWifiVendorHal.installPacketFilter(filter);
+ return mWifiVendorHal.installPacketFilter(mInterfaceName, filter);
}
/**
@@ -1211,7 +1852,7 @@ public class WifiNative {
* @return true for success
*/
public boolean setCountryCodeHal(String countryCode) {
- return mWifiVendorHal.setCountryCodeHal(countryCode);
+ return mWifiVendorHal.setCountryCodeHal(mInterfaceName, countryCode);
}
//---------------------------------------------------------------------------------
@@ -1551,7 +2192,7 @@ public class WifiNative {
* @return true for success, false otherwise.
*/
public boolean startPktFateMonitoring() {
- return mWifiVendorHal.startPktFateMonitoring();
+ return mWifiVendorHal.startPktFateMonitoring(mInterfaceName);
}
/**
@@ -1560,14 +2201,14 @@ public class WifiNative {
* @return true for success, false otherwise.
*/
public boolean getTxPktFates(TxFateReport[] reportBufs) {
- return mWifiVendorHal.getTxPktFates(reportBufs);
+ return mWifiVendorHal.getTxPktFates(mInterfaceName, reportBufs);
}
/**
* Fetch the most recent RX packet fates from the HAL. Fails unless HAL is started.
*/
public boolean getRxPktFates(RxFateReport[] reportBufs) {
- return mWifiVendorHal.getRxPktFates(reportBufs);
+ return mWifiVendorHal.getRxPktFates(mInterfaceName, reportBufs);
}
/**
@@ -1586,7 +2227,7 @@ public class WifiNative {
Integer hexVal = Integer.parseInt(macAddrStr[i], 16);
srcMac[i] = hexVal.byteValue();
}
- return mWifiVendorHal.startSendingOffloadedPacket(
+ return mWifiVendorHal.startSendingOffloadedPacket(mInterfaceName,
slot, srcMac, keepAlivePacket, period);
}
@@ -1597,7 +2238,7 @@ public class WifiNative {
* @return 0 for success, -1 for error
*/
public int stopSendingOffloadedPacket(int slot) {
- return mWifiVendorHal.stopSendingOffloadedPacket(slot);
+ return mWifiVendorHal.stopSendingOffloadedPacket(mInterfaceName, slot);
}
public static interface WifiRssiEventHandler {
@@ -1614,11 +2255,12 @@ public class WifiNative {
*/
public int startRssiMonitoring(byte maxRssi, byte minRssi,
WifiRssiEventHandler rssiEventHandler) {
- return mWifiVendorHal.startRssiMonitoring(maxRssi, minRssi, rssiEventHandler);
+ return mWifiVendorHal.startRssiMonitoring(
+ mInterfaceName, maxRssi, minRssi, rssiEventHandler);
}
public int stopRssiMonitoring() {
- return mWifiVendorHal.stopRssiMonitoring();
+ return mWifiVendorHal.stopRssiMonitoring(mInterfaceName);
}
/**
@@ -1637,7 +2279,7 @@ public class WifiNative {
* @return true for success, false otherwise.
*/
public boolean configureNeighborDiscoveryOffload(boolean enabled) {
- return mWifiVendorHal.configureNeighborDiscoveryOffload(enabled);
+ return mWifiVendorHal.configureNeighborDiscoveryOffload(mInterfaceName, enabled);
}
// Firmware roaming control.
@@ -1655,7 +2297,7 @@ public class WifiNative {
* @return true for success, false otherwise.
*/
public boolean getRoamingCapabilities(RoamingCapabilities capabilities) {
- return mWifiVendorHal.getRoamingCapabilities(capabilities);
+ return mWifiVendorHal.getRoamingCapabilities(mInterfaceName, capabilities);
}
/**
@@ -1670,7 +2312,7 @@ public class WifiNative {
* @return error code returned from HAL.
*/
public int enableFirmwareRoaming(int state) {
- return mWifiVendorHal.enableFirmwareRoaming(state);
+ return mWifiVendorHal.enableFirmwareRoaming(mInterfaceName, state);
}
/**
@@ -1686,7 +2328,7 @@ public class WifiNative {
*/
public boolean configureRoaming(RoamingConfig config) {
Log.d(mTAG, "configureRoaming ");
- return mWifiVendorHal.configureRoaming(config);
+ return mWifiVendorHal.configureRoaming(mInterfaceName, config);
}
/**
@@ -1695,7 +2337,7 @@ public class WifiNative {
public boolean resetRoamingConfiguration() {
// Pass in an empty RoamingConfig object which translates to zero size
// blacklist and whitelist to reset the firmware roaming configuration.
- return mWifiVendorHal.configureRoaming(new RoamingConfig());
+ return mWifiVendorHal.configureRoaming(mInterfaceName, new RoamingConfig());
}
/**
diff --git a/com/android/server/wifi/WifiNetworkSelector.java b/com/android/server/wifi/WifiNetworkSelector.java
index 071fc07d..46ded1cc 100644
--- a/com/android/server/wifi/WifiNetworkSelector.java
+++ b/com/android/server/wifi/WifiNetworkSelector.java
@@ -152,9 +152,8 @@ public class WifiNetworkSelector {
boolean hasQualifiedRssi =
(wifiInfo.is24GHz() && (currentRssi > mThresholdQualifiedRssi24))
|| (wifiInfo.is5GHz() && (currentRssi > mThresholdQualifiedRssi5));
- // getTxSuccessRate() and getRxSuccessRate() returns the packet rate in per 5 seconds unit.
- boolean hasActiveStream = (wifiInfo.getTxSuccessRatePps() > mStayOnNetworkMinimumTxRate)
- || (wifiInfo.getRxSuccessRatePps() > mStayOnNetworkMinimumRxRate);
+ boolean hasActiveStream = (wifiInfo.txSuccessRate > mStayOnNetworkMinimumTxRate)
+ || (wifiInfo.rxSuccessRate > mStayOnNetworkMinimumRxRate);
if (hasQualifiedRssi && hasActiveStream) {
localLog("Stay on current network because of good RSSI and ongoing traffic");
return true;
diff --git a/com/android/server/wifi/WifiScoreReport.java b/com/android/server/wifi/WifiScoreReport.java
index e5281ef7..1c258e1a 100644
--- a/com/android/server/wifi/WifiScoreReport.java
+++ b/com/android/server/wifi/WifiScoreReport.java
@@ -216,7 +216,7 @@ public class WifiScoreReport {
history = new LinkedList<>(mLinkMetricsHistory);
}
pw.println("time,session,rssi,filtered_rssi,rssi_threshold,"
- + "freq,linkspeed,tx_good,tx_retry,tx_bad,rx,s0,s1,s2");
+ + "freq,linkspeed,tx_good,tx_retry,tx_bad,rx_pps,s0,s1,s2");
for (String line : history) {
pw.println(line);
}
diff --git a/com/android/server/wifi/WifiServiceImpl.java b/com/android/server/wifi/WifiServiceImpl.java
index 8db180f1..ce49e559 100644
--- a/com/android/server/wifi/WifiServiceImpl.java
+++ b/com/android/server/wifi/WifiServiceImpl.java
@@ -69,7 +69,6 @@ import android.net.wifi.WifiActivityEnergyInfo;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConnectionStatistics;
import android.net.wifi.WifiInfo;
-import android.net.wifi.WifiLinkLayerStats;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.LocalOnlyHotspotCallback;
import android.net.wifi.WifiScanner;
@@ -98,6 +97,7 @@ import android.provider.Settings;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.MutableInt;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
@@ -171,8 +171,6 @@ public class WifiServiceImpl extends IWifiManager.Stub {
final WifiSettingsStore mSettingsStore;
/* Logs connection events and some general router and scan stats */
private final WifiMetrics mWifiMetrics;
- /* Manages affiliated certificates for current user */
- private final WifiCertManager mCertManager;
private final WifiInjector mWifiInjector;
/* Backup/Restore Module */
@@ -202,6 +200,18 @@ public class WifiServiceImpl extends IWifiManager.Stub {
private final ConcurrentHashMap<String, Integer> mIfaceIpModes;
/**
+ * One of: {@link WifiManager#WIFI_AP_STATE_DISABLED},
+ * {@link WifiManager#WIFI_AP_STATE_DISABLING},
+ * {@link WifiManager#WIFI_AP_STATE_ENABLED},
+ * {@link WifiManager#WIFI_AP_STATE_ENABLING},
+ * {@link WifiManager#WIFI_AP_STATE_FAILED}
+ *
+ * Access/maintenance MUST be done on the wifi service thread
+ */
+ private int mWifiApState = WifiManager.WIFI_AP_STATE_DISABLED;
+
+
+ /**
* Callback for use with LocalOnlyHotspot to unregister requesting applications upon death.
*
* @hide
@@ -434,7 +444,6 @@ public class WifiServiceImpl extends IWifiManager.Stub {
mPowerManager = mContext.getSystemService(PowerManager.class);
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
- mCertManager = mWifiInjector.getWifiCertManager();
mWifiLockManager = mWifiInjector.getWifiLockManager();
mWifiMulticastLockManager = mWifiInjector.getWifiMulticastLockManager();
HandlerThread wifiServiceHandlerThread = mWifiInjector.getWifiServiceHandlerThread();
@@ -791,8 +800,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
}
// If SoftAp is enabled, only Settings is allowed to toggle wifi
- boolean apEnabled =
- mWifiStateMachine.syncGetWifiApState() != WifiManager.WIFI_AP_STATE_DISABLED;
+ boolean apEnabled = mWifiApState != WifiManager.WIFI_AP_STATE_DISABLED;
if (apEnabled && !isFromSettings) {
mLog.info("setWifiEnabled SoftAp not disabled: only Settings can enable wifi").flush();
@@ -864,7 +872,13 @@ public class WifiServiceImpl extends IWifiManager.Stub {
public int getWifiApEnabledState() {
enforceAccessPermission();
mLog.info("getWifiApEnabledState uid=%").c(Binder.getCallingUid()).flush();
- return mWifiStateMachine.syncGetWifiApState();
+
+ // hand off work to our handler thread
+ MutableInt apState = new MutableInt(WifiManager.WIFI_AP_STATE_DISABLED);
+ mClientHandler.runWithScissors(() -> {
+ apState.value = mWifiApState;
+ }, 0);
+ return apState.value;
}
/**
@@ -1025,6 +1039,8 @@ public class WifiServiceImpl extends IWifiManager.Stub {
/**
* Private method to handle SoftAp state changes
+ *
+ * <p> MUST be called from the WifiStateMachine thread.
*/
private void handleWifiApStateChange(
int currentState, int previousState, int errorCode, String ifaceName, int mode) {
@@ -1033,6 +1049,9 @@ public class WifiServiceImpl extends IWifiManager.Stub {
+ " previousState=" + previousState + " errorCode= " + errorCode
+ " ifaceName=" + ifaceName + " mode=" + mode);
+ // update the tracking ap state variable
+ mWifiApState = currentState;
+
// check if we have a failure - since it is possible (worst case scenario where
// WifiController and WifiStateMachine are out of sync wrt modes) to get two FAILED
// notifications in a row, we need to handle this first.
@@ -1734,12 +1753,19 @@ public class WifiServiceImpl extends IWifiManager.Stub {
@Override
public WifiInfo getConnectionInfo(String callingPackage) {
enforceAccessPermission();
- mLog.info("getConnectionInfo uid=%").c(Binder.getCallingUid()).flush();
+ int uid = Binder.getCallingUid();
+ mLog.info("getConnectionInfo uid=%").c(uid).flush();
/*
* Make sure we have the latest information, by sending
* a status request to the supplicant.
*/
- return mWifiStateMachine.syncRequestConnectionInfo(callingPackage);
+ long ident = Binder.clearCallingIdentity();
+ try {
+ WifiInfo result = mWifiStateMachine.syncRequestConnectionInfo(callingPackage, uid);
+ return result;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
}
/**
@@ -2381,9 +2407,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
mWifiStateMachine.enableVerboseLogging(verbose);
mWifiLockManager.enableVerboseLogging(verbose);
mWifiMulticastLockManager.enableVerboseLogging(verbose);
- mWifiInjector.getWifiLastResortWatchdog().enableVerboseLogging(verbose);
- mWifiInjector.getWifiBackupRestore().enableVerboseLogging(verbose);
- LogcatLog.enableVerboseLogging(verbose);
+ mWifiInjector.enableVerboseLogging(verbose);
}
@Override
@@ -2411,38 +2435,6 @@ public class WifiServiceImpl extends IWifiManager.Stub {
return mWifiStateMachine.getAggressiveHandover();
}
- @Override
- public void setAllowScansWithTraffic(int enabled) {
- enforceAccessPermission();
- mLog.info("setAllowScansWithTraffic uid=% enabled=%")
- .c(Binder.getCallingUid())
- .c(enabled).flush();
- mWifiStateMachine.setAllowScansWithTraffic(enabled);
- }
-
- @Override
- public int getAllowScansWithTraffic() {
- enforceAccessPermission();
- mLog.info("getAllowScansWithTraffic uid=%").c(Binder.getCallingUid()).flush();
- return mWifiStateMachine.getAllowScansWithTraffic();
- }
-
- @Override
- public boolean setEnableAutoJoinWhenAssociated(boolean enabled) {
- enforceChangePermission();
- mLog.info("setEnableAutoJoinWhenAssociated uid=% enabled=%")
- .c(Binder.getCallingUid())
- .c(enabled).flush();
- return mWifiStateMachine.setEnableAutoJoinWhenAssociated(enabled);
- }
-
- @Override
- public boolean getEnableAutoJoinWhenAssociated() {
- enforceAccessPermission();
- mLog.info("getEnableAutoJoinWhenAssociated uid=%").c(Binder.getCallingUid()).flush();
- return mWifiStateMachine.getEnableAutoJoinWhenAssociated();
- }
-
/* Return the Wifi Connection statistics object */
@Override
public WifiConnectionStatistics getConnectionStatistics() {
@@ -2550,14 +2542,6 @@ public class WifiServiceImpl extends IWifiManager.Stub {
return sb.toString();
}
- public void hideCertFromUnaffiliatedUsers(String alias) {
- mCertManager.hideCertFromUnaffiliatedUsers(alias);
- }
-
- public String[] listClientCertsForCurrentUser() {
- return mCertManager.listClientCertsForCurrentUser();
- }
-
/**
* Enable/disable WifiConnectivityManager at runtime
*
diff --git a/com/android/server/wifi/WifiStateMachine.java b/com/android/server/wifi/WifiStateMachine.java
index b005923d..47b4b151 100644
--- a/com/android/server/wifi/WifiStateMachine.java
+++ b/com/android/server/wifi/WifiStateMachine.java
@@ -29,9 +29,7 @@ import static android.net.wifi.WifiManager.WIFI_STATE_UNKNOWN;
import static android.telephony.TelephonyManager.CALL_STATE_IDLE;
import static android.telephony.TelephonyManager.CALL_STATE_OFFHOOK;
-import android.Manifest;
import android.app.ActivityManager;
-import android.app.AppGlobals;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
@@ -39,7 +37,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.net.ConnectivityManager;
@@ -60,7 +57,6 @@ import android.net.StaticIpConfiguration;
import android.net.TrafficStats;
import android.net.dhcp.DhcpClient;
import android.net.ip.IpClient;
-import android.net.wifi.IApInterface;
import android.net.wifi.IClientInterface;
import android.net.wifi.RssiPacketCountInfo;
import android.net.wifi.ScanResult;
@@ -71,7 +67,6 @@ import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConnectionStatistics;
import android.net.wifi.WifiEnterpriseConfig;
import android.net.wifi.WifiInfo;
-import android.net.wifi.WifiLinkLayerStats;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiScanner;
import android.net.wifi.WifiSsid;
@@ -83,7 +78,6 @@ import android.net.wifi.hotspot2.OsuProvider;
import android.net.wifi.hotspot2.PasspointConfiguration;
import android.net.wifi.p2p.IWifiP2pManager;
import android.os.BatteryStats;
-import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@@ -129,6 +123,7 @@ import com.android.server.wifi.util.TelephonyUtil;
import com.android.server.wifi.util.TelephonyUtil.SimAuthRequestData;
import com.android.server.wifi.util.TelephonyUtil.SimAuthResponseData;
import com.android.server.wifi.util.WifiPermissionsUtil;
+import com.android.server.wifi.util.WifiPermissionsWrapper;
import java.io.BufferedReader;
import java.io.FileDescriptor;
@@ -185,7 +180,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private static final String EXTRA_OSU_PROVIDER = "OsuProvider";
private boolean mVerboseLoggingEnabled = false;
-
+ private final WifiPermissionsWrapper mWifiPermissionsWrapper;
/* debug flag, indicating if handling of ASSOCIATION_REJECT ended up blacklisting
* the corresponding BSSID.
*/
@@ -252,8 +247,9 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private String mLastBssid;
private int mLastNetworkId; // The network Id we successfully joined
private boolean mIsLinkDebouncing = false;
- private final StateMachineDeathRecipient mDeathRecipient =
- new StateMachineDeathRecipient(this, CMD_CLIENT_INTERFACE_BINDER_DEATH);
+ private final WifiNative.WificondDeathEventHandler mWificondDeathRecipient = () -> {
+ sendMessage(CMD_WIFICOND_BINDER_DEATH);
+ };
private final WifiNative.VendorHalDeathEventHandler mVendorHalDeathRecipient = () -> {
sendMessage(CMD_VENDOR_HAL_HWBINDER_DEATH);
};
@@ -369,7 +365,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private DhcpResults mDhcpResults;
// NOTE: Do not return to clients - see syncRequestConnectionInfo()
- private final WifiInfo mWifiInfo;
+ private final ExtendedWifiInfo mWifiInfo;
private NetworkInfo mNetworkInfo;
private final NetworkCapabilities mDfltNetworkCapabilities;
private SupplicantStateTracker mSupplicantStateTracker;
@@ -700,8 +696,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
/* Enable/Disable WifiConnectivityManager */
static final int CMD_ENABLE_WIFI_CONNECTIVITY_MANAGER = BASE + 166;
- /* Enable/Disable AutoJoin when associated */
- static final int CMD_ENABLE_AUTOJOIN_WHEN_ASSOCIATED = BASE + 167;
/* Get all matching Passpoint configurations */
static final int CMD_GET_ALL_MATCHING_CONFIGS = BASE + 168;
@@ -730,8 +724,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
/* used to indicate that the foreground user was switched */
static final int CMD_USER_STOP = BASE + 207;
- /* Signals that IClientInterface instance underpinning our state is dead. */
- private static final int CMD_CLIENT_INTERFACE_BINDER_DEATH = BASE + 250;
+ /* Signals that wificond is dead. */
+ private static final int CMD_WIFICOND_BINDER_DEATH = BASE + 250;
/* Signals that the Vendor HAL instance underpinning our state is dead. */
private static final int CMD_VENDOR_HAL_HWBINDER_DEATH = BASE + 251;
@@ -799,8 +793,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
*/
private long mSupplicantScanIntervalMs;
- private boolean mEnableAutoJoinWhenAssociated;
- private int mAlwaysEnableScansWhileAssociated;
private final int mThresholdQualifiedRssi24;
private final int mThresholdQualifiedRssi5;
private final int mThresholdSaturatedRssi24;
@@ -951,8 +943,9 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mWifiMonitor = mWifiInjector.getWifiMonitor();
mWifiDiagnostics = mWifiInjector.makeWifiDiagnostics(mWifiNative);
+ mWifiPermissionsWrapper = mWifiInjector.getWifiPermissionsWrapper();
- mWifiInfo = new WifiInfo();
+ mWifiInfo = new ExtendedWifiInfo();
mSupplicantStateTracker =
mFacade.makeSupplicantStateTracker(context, mWifiConfigManager, getHandler());
@@ -1040,8 +1033,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
com.android.internal.R.string.config_wifi_tcp_buffers);
// Load Device configs
- mEnableAutoJoinWhenAssociated = context.getResources().getBoolean(
- R.bool.config_wifi_framework_enable_associated_network_selection);
mThresholdQualifiedRssi24 = context.getResources().getInteger(
R.integer.config_wifi_framework_wifi_score_low_rssi_threshold_24GHz);
mThresholdQualifiedRssi5 = context.getResources().getInteger(
@@ -1280,26 +1271,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// mWifiConfigManager.trimANQPCache(true);
}
- public void setAllowScansWithTraffic(int enabled) {
- mAlwaysEnableScansWhileAssociated = enabled;
- }
-
- public int getAllowScansWithTraffic() {
- return mAlwaysEnableScansWhileAssociated;
- }
-
- /*
- * Dynamically turn on/off if switching networks while connected is allowd.
- */
- public boolean setEnableAutoJoinWhenAssociated(boolean enabled) {
- sendMessage(CMD_ENABLE_AUTOJOIN_WHEN_ASSOCIATED, enabled ? 1 : 0);
- return true;
- }
-
- public boolean getEnableAutoJoinWhenAssociated() {
- return mEnableAutoJoinWhenAssociated;
- }
-
private boolean setRandomMacOui() {
String oui = mContext.getResources().getString(R.string.config_wifi_random_mac_oui);
if (TextUtils.isEmpty(oui)) {
@@ -1472,9 +1443,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
WifiLinkLayerStats getWifiLinkLayerStats() {
WifiLinkLayerStats stats = null;
if (mWifiLinkLayerStatsSupported > 0) {
- String name = "wlan0";
- stats = mWifiNative.getWifiLinkLayerStats(name);
- if (name != null && stats == null && mWifiLinkLayerStatsSupported > 0) {
+ stats = mWifiNative.getWifiLinkLayerStats();
+ if (stats == null && mWifiLinkLayerStatsSupported > 0) {
mWifiLinkLayerStatsSupported -= 1;
} else if (stats != null) {
lastLinkLayerStatsUpdate = mClock.getWallClockMillis();
@@ -1705,9 +1675,9 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
/**
* TODO: doc
*/
- public int syncGetWifiApState() {
- return mWifiApState.get();
- }
+ //public int syncGetWifiApState() {
+ // return mWifiApState.get();
+ //}
/**
* TODO: doc
@@ -1764,20 +1734,18 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
/**
* Get status information for the current connection, if any.
*
+ * @param callingPackage string indicating the calling package of the caller
+ * @param uid the calling uid
* @return a {@link WifiInfo} object containing information about the current connection
*/
- public WifiInfo syncRequestConnectionInfo(String callingPackage) {
- int uid = Binder.getCallingUid();
+ public WifiInfo syncRequestConnectionInfo(String callingPackage, int uid) {
WifiInfo result = new WifiInfo(mWifiInfo);
- if (uid == Process.myUid()) return result;
boolean hideBssidAndSsid = true;
result.setMacAddress(WifiInfo.DEFAULT_MAC_ADDRESS);
- IPackageManager packageManager = AppGlobals.getPackageManager();
-
try {
- if (packageManager.checkUidPermission(Manifest.permission.LOCAL_MAC_ADDRESS,
- uid) == PackageManager.PERMISSION_GRANTED) {
+ if (mWifiPermissionsWrapper.getLocalMacAddressPermission(uid)
+ == PackageManager.PERMISSION_GRANTED) {
result.setMacAddress(mWifiInfo.getMacAddress());
}
if (mWifiPermissionsUtil.canAccessScanResults(
@@ -2276,16 +2244,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
pw.println("mOperationalMode " + mOperationalMode);
pw.println("mUserWantsSuspendOpt " + mUserWantsSuspendOpt);
pw.println("mSuspendOptNeedsDisabled " + mSuspendOptNeedsDisabled);
- if (mCountryCode.getCountryCodeSentToDriver() != null) {
- pw.println("CountryCode sent to driver " + mCountryCode.getCountryCodeSentToDriver());
- } else {
- if (mCountryCode.getCountryCode() != null) {
- pw.println("CountryCode: " +
- mCountryCode.getCountryCode() + " was not sent to driver");
- } else {
- pw.println("CountryCode was not initialized");
- }
- }
+ mCountryCode.dump(fd, pw, args);
+
if (mNetworkFactory != null) {
mNetworkFactory.dump(fd, pw, args);
} else {
@@ -2312,6 +2272,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
} else {
pw.println("mWifiConnectivityManager is not initialized");
}
+ mWifiInjector.getWakeupController().dump(fd, pw, args);
}
public void handleUserSwitch(int userId) {
@@ -2950,6 +2911,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// Update state
mWifiApState.set(wifiApState);
+ // TODO: when this code is removed, also remove syncGetWifiApStateByName()
if (mVerboseLoggingEnabled) log("setWifiApState: " + syncGetWifiApStateByName());
}
@@ -3280,16 +3242,18 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
|| stateChangeResult.wifiSsid.toString().isEmpty()) && isLinkDebouncing()) {
return state;
}
- // Network id is only valid when we start connecting
+ // Network id and SSID are only valid when we start connecting
if (SupplicantState.isConnecting(state)) {
mWifiInfo.setNetworkId(lookupFrameworkNetworkId(stateChangeResult.networkId));
+ mWifiInfo.setBSSID(stateChangeResult.BSSID);
+ mWifiInfo.setSSID(stateChangeResult.wifiSsid);
} else {
+ // Reset parameters according to WifiInfo.reset()
mWifiInfo.setNetworkId(WifiConfiguration.INVALID_NETWORK_ID);
+ mWifiInfo.setBSSID(null);
+ mWifiInfo.setSSID(null);
}
- mWifiInfo.setBSSID(stateChangeResult.BSSID);
- mWifiInfo.setSSID(stateChangeResult.wifiSsid);
-
final WifiConfiguration config = getCurrentWifiConfiguration();
if (config != null) {
mWifiInfo.setEphemeral(config.ephemeral);
@@ -4139,7 +4103,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mWifiNative.stopFilteringMulticastV4Packets();
}
break;
- case CMD_CLIENT_INTERFACE_BINDER_DEATH:
+ case CMD_WIFICOND_BINDER_DEATH:
Log.e(TAG, "wificond died unexpectedly. Triggering recovery");
mWifiMetrics.incrementNumWificondCrashes();
mWifiDiagnostics.captureBugReportData(
@@ -4178,7 +4142,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// Tearing down the client interfaces below is going to stop our supplicant.
mWifiMonitor.stopAllMonitoring();
- mDeathRecipient.unlinkToDeath();
+ // stop hostapd in case it was running from SoftApMode
+ mWifiNative.stopSoftAp();
+
+ mWifiNative.deregisterWificondDeathHandler();
mWifiNative.tearDown();
}
@@ -4201,7 +4168,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
incrementMetricsForSetupFailure(statusAndInterface.first);
}
if (mClientInterface == null
- || !mDeathRecipient.linkToDeath(mClientInterface.asBinder())) {
+ || !mWifiNative.registerWificondDeathHandler(mWificondDeathRecipient)) {
setWifiState(WifiManager.WIFI_STATE_UNKNOWN);
cleanup();
break;
@@ -4324,9 +4291,12 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
transitionTo(mInitialState);
}
break;
+ case CMD_START_AP:
+ // now go directly to softap mode since we handle teardown in WSMP
+ transitionTo(mSoftApState);
+ break;
case CMD_START_SUPPLICANT:
case CMD_STOP_SUPPLICANT:
- case CMD_START_AP:
case CMD_STOP_AP:
case CMD_SET_OPERATIONAL_MODE:
messageHandlingStatus = MESSAGE_HANDLING_STATUS_DEFERRED;
@@ -4476,10 +4446,12 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
sendMessage(mBufferedScanMsg.remove());
break;
case CMD_START_AP:
- /* Cannot start soft AP while in client mode */
- loge("Failed to start soft AP with a running supplicant");
- setWifiApState(WIFI_AP_STATE_FAILED, WifiManager.SAP_START_FAILURE_GENERAL,
- null, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ // /* Cannot start soft AP while in client mode */
+ // loge("Failed to start soft AP with a running supplicant");
+ // setWifiApState(WIFI_AP_STATE_FAILED, WifiManager.SAP_START_FAILURE_GENERAL,
+ // null, WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ // now go directly to softap mode since we handle teardown in WSMP
+ transitionTo(mSoftApState);
break;
case CMD_SET_OPERATIONAL_MODE:
mOperationalMode = message.arg1;
@@ -4558,15 +4530,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
case CMD_ENABLE_WIFI_CONNECTIVITY_MANAGER:
mWifiConnectivityManager.enable(message.arg1 == 1 ? true : false);
break;
- case CMD_ENABLE_AUTOJOIN_WHEN_ASSOCIATED:
- final boolean allowed = (message.arg1 > 0);
- boolean old_state = mEnableAutoJoinWhenAssociated;
- mEnableAutoJoinWhenAssociated = allowed;
- if (!old_state && allowed && mScreenOn
- && getCurrentState() == mConnectedState) {
- mWifiConnectivityManager.forceConnectivityScan(WIFI_WORK_SOURCE);
- }
- break;
case CMD_SELECT_TX_POWER_SCENARIO:
int txPowerScenario = message.arg1;
logd("Setting Tx power scenario to " + txPowerScenario);
@@ -4686,7 +4649,14 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
public void enter() {
mLastOperationMode = mOperationalMode;
mWifiStateTracker.updateState(WifiStateTracker.SCAN_MODE);
+ mWifiInjector.getWakeupController().start();
}
+
+ @Override
+ public void exit() {
+ mWifiInjector.getWakeupController().stop();
+ }
+
@Override
public boolean processMessage(Message message) {
logStateAndMessage(message, this);
@@ -4914,6 +4884,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// Let the system know that wifi is available in client mode.
setWifiState(WIFI_STATE_ENABLED);
+ mWifiInjector.getWakeupController().reset();
+
mNetworkInfo.setIsAvailable(true);
if (mNetworkAgent != null) mNetworkAgent.sendNetworkInfo(mNetworkInfo);
@@ -4944,6 +4916,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
mWifiInfo.reset();
mWifiInfo.setSupplicantState(SupplicantState.DISCONNECTED);
+ setWifiState(WIFI_STATE_DISABLED);
}
@Override
@@ -6982,6 +6955,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private String mIfaceName;
private int mMode;
+ /*
private class SoftApListener implements SoftApManager.Listener {
@Override
public void onStateChanged(int state, int reason) {
@@ -6994,6 +6968,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
setWifiApState(state, reason, mIfaceName, mMode);
}
}
+ */
@Override
public void enter() {
@@ -7001,6 +6976,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (message.what != CMD_START_AP) {
throw new RuntimeException("Illegal transition to SoftApState: " + message);
}
+ /*
SoftApModeConfiguration config = (SoftApModeConfiguration) message.obj;
mMode = config.getTargetMode();
@@ -7034,11 +7010,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
checkAndSetConnectivityInstance();
mSoftApManager = mWifiInjector.makeSoftApManager(mNwService,
new SoftApListener(),
- apInterface,
- mIfaceName,
config);
mSoftApManager.start();
mWifiStateTracker.updateState(WifiStateTracker.SOFT_AP);
+ */
}
@Override
@@ -7057,7 +7032,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
/* Ignore start command when it is starting/started. */
break;
case CMD_STOP_AP:
- mSoftApManager.stop();
+ //mSoftApManager.stop();
+ transitionTo(mInitialState);
break;
case CMD_START_AP_FAILURE:
transitionTo(mInitialState);
diff --git a/com/android/server/wifi/WifiStateMachinePrime.java b/com/android/server/wifi/WifiStateMachinePrime.java
index c49b6450..2d3aaba1 100644
--- a/com/android/server/wifi/WifiStateMachinePrime.java
+++ b/com/android/server/wifi/WifiStateMachinePrime.java
@@ -17,14 +17,11 @@
package com.android.server.wifi;
import android.annotation.NonNull;
-import android.net.wifi.IApInterface;
-import android.net.wifi.IWificond;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.os.INetworkManagementService;
import android.os.Looper;
import android.os.Message;
-import android.os.RemoteException;
import android.util.Log;
import com.android.internal.util.Protocol;
@@ -47,12 +44,13 @@ public class WifiStateMachinePrime {
private final WifiInjector mWifiInjector;
private final Looper mLooper;
+ private final WifiNative mWifiNative;
private final INetworkManagementService mNMService;
- private IWificond mWificond;
-
private Queue<SoftApModeConfiguration> mApConfigQueue = new ConcurrentLinkedQueue<>();
+ private String mInterfaceName;
+
/* The base for wifi message types */
static final int BASE = Protocol.BASE_WIFI;
@@ -67,22 +65,17 @@ public class WifiStateMachinePrime {
WifiStateMachinePrime(WifiInjector wifiInjector,
Looper looper,
+ WifiNative wifiNative,
INetworkManagementService nmService) {
mWifiInjector = wifiInjector;
mLooper = looper;
+ mWifiNative = wifiNative;
mNMService = nmService;
- // Clean up existing interfaces in wificond.
- // This ensures that the framework and wificond are in a consistent state after a framework
- // restart.
- try {
- mWificond = mWifiInjector.makeWificond();
- if (mWificond != null) {
- mWificond.tearDownInterfaces();
- }
- } catch (RemoteException e) {
- Log.e(TAG, "wificond died during framework startup");
- }
+ mInterfaceName = mWifiNative.getInterfaceName();
+
+ // make sure we do not have leftover state in the event of a restart
+ mWifiNative.tearDown();
}
/**
@@ -211,23 +204,14 @@ public class WifiStateMachinePrime {
return HANDLED;
}
- private void tearDownInterfaces() {
- if (mWificond != null) {
- try {
- mWificond.tearDownInterfaces();
- } catch (RemoteException e) {
- // There is very little we can do here
- Log.e(TAG, "Failed to tear down interfaces via wificond");
- }
- mWificond = null;
- }
- return;
+ private void cleanup() {
+ mWifiNative.disableSupplicant();
+ mWifiNative.tearDown();
}
class ClientModeState extends State {
@Override
public void enter() {
- mWificond = mWifiInjector.makeWificond();
}
@Override
@@ -240,7 +224,7 @@ public class WifiStateMachinePrime {
@Override
public void exit() {
- tearDownInterfaces();
+ cleanup();
}
}
@@ -266,33 +250,18 @@ public class WifiStateMachinePrime {
}
class SoftAPModeState extends State {
- IApInterface mApInterface = null;
- String mIfaceName = null;
@Override
public void enter() {
+ // For now - need to clean up from other mode management in WSM
+ cleanup();
+
final Message message = mModeStateMachine.getCurrentMessage();
if (message.what != ModeStateMachine.CMD_START_SOFT_AP_MODE) {
Log.d(TAG, "Entering SoftAPMode (idle)");
return;
}
- // Continue with setup since we are changing modes
- mApInterface = null;
-
- try {
- mWificond = mWifiInjector.makeWificond();
- mApInterface = mWificond.createApInterface(
- mWifiInjector.getWifiNative().getInterfaceName());
- mIfaceName = mApInterface.getInterfaceName();
- } catch (RemoteException e) {
- initializationFailed(
- "Could not get IApInterface instance or its name from wificond");
- return;
- } catch (NullPointerException e) {
- initializationFailed("wificond failure when initializing softap mode");
- return;
- }
mModeStateMachine.transitionTo(mSoftAPModeActiveState);
}
@@ -310,9 +279,8 @@ public class WifiStateMachinePrime {
// not in active state, nothing to stop.
break;
case CMD_START_AP_FAILURE:
- // remove the saved config for the start attempt
- mApConfigQueue.poll();
- Log.e(TAG, "Failed to start SoftApMode. Wait for next mode command.");
+ // with interface management in softapmanager, no setup failures can be seen
+ // here
break;
case CMD_AP_STOPPED:
Log.d(TAG, "SoftApModeActiveState stopped. Wait for next mode command.");
@@ -325,15 +293,9 @@ public class WifiStateMachinePrime {
@Override
public void exit() {
- tearDownInterfaces();
- }
-
- protected IApInterface getInterface() {
- return mApInterface;
- }
-
- protected String getInterfaceName() {
- return mIfaceName;
+ // while in transition, cleanup is done on entering states. in the future, each
+ // mode will clean up their own state on exit
+ //cleanup();
}
private void initializationFailed(String message) {
@@ -345,8 +307,9 @@ public class WifiStateMachinePrime {
class WifiDisabledState extends State {
@Override
public void enter() {
- // make sure everything is torn down
Log.d(TAG, "Entering WifiDisabledState");
+ // make sure everything is torn down
+ cleanup();
}
@Override
@@ -415,9 +378,8 @@ public class WifiStateMachinePrime {
config = null;
}
this.mActiveModeManager = mWifiInjector.makeSoftApManager(mNMService,
- new SoftApListener(), ((SoftAPModeState) mSoftAPModeState).getInterface(),
- ((SoftAPModeState) mSoftAPModeState).getInterfaceName(), softApModeConfig);
- mActiveModeManager.start();
+ new SoftApListener(), softApModeConfig);
+ this.mActiveModeManager.start();
}
@Override
diff --git a/com/android/server/wifi/WifiVendorHal.java b/com/android/server/wifi/WifiVendorHal.java
index 3f39e458..d6b568d5 100644
--- a/com/android/server/wifi/WifiVendorHal.java
+++ b/com/android/server/wifi/WifiVendorHal.java
@@ -59,7 +59,6 @@ import android.net.wifi.RttManager;
import android.net.wifi.RttManager.ResponderConfig;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiInfo;
-import android.net.wifi.WifiLinkLayerStats;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiScanner;
import android.net.wifi.WifiSsid;
@@ -67,6 +66,7 @@ import android.net.wifi.WifiWakeReasonAndCounts;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
+import android.text.TextUtils;
import android.util.Log;
import android.util.MutableBoolean;
import android.util.MutableInt;
@@ -74,10 +74,14 @@ import android.util.MutableInt;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.server.connectivity.KeepalivePacketData;
+import com.android.server.wifi.HalDeviceManager.InterfaceDestroyedListener;
import com.android.server.wifi.util.BitMask;
import com.android.server.wifi.util.NativeUtil;
+import libcore.util.NonNull;
+
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.Set;
/**
@@ -139,7 +143,7 @@ public class WifiVendorHal {
}
/**
- * Logs if the argument is false.
+ * Logs the argument along with the method name.
*
* Always returns its argument.
*/
@@ -159,6 +163,26 @@ public class WifiVendorHal {
}
/**
+ * Logs the argument along with the method name.
+ *
+ * Always returns its argument.
+ */
+ private String stringResult(String result) {
+ if (mVerboseLog == sNoLog) return result;
+ // Currently only seen if verbose logging is on
+
+ Thread cur = Thread.currentThread();
+ StackTraceElement[] trace = cur.getStackTrace();
+
+ mVerboseLog.err("% returns %")
+ .c(niceMethodName(trace, 3))
+ .c(result)
+ .flush();
+
+ return result;
+ }
+
+ /**
* Logs at method entry
*
* @param format string with % placeholders
@@ -201,9 +225,9 @@ public class WifiVendorHal {
// Vendor HAL HIDL interface objects.
private IWifiChip mIWifiChip;
- private IWifiStaIface mIWifiStaIface;
- private IWifiApIface mIWifiApIface;
private IWifiRttController mIWifiRttController;
+ private HashMap<String, IWifiStaIface> mIWifiStaIfaces = new HashMap<>();
+ private HashMap<String, IWifiApIface> mIWifiApIfaces = new HashMap<>();
private final HalDeviceManager mHalDeviceManager;
private final HalDeviceManagerStatusListener mHalDeviceManagerStatusCallbacks;
private final IWifiStaIfaceEventCallback mIWifiStaIfaceEventCallback;
@@ -271,7 +295,16 @@ public class WifiVendorHal {
* @return true for success
*/
public boolean startVendorHalAp() {
- return startVendorHal(AP_MODE);
+ synchronized (sLock) {
+ if (!startVendorHal()) {
+ return false;
+ }
+ if (TextUtils.isEmpty(createApIface(null))) {
+ stopVendorHal();
+ return false;
+ }
+ return true;
+ }
}
/**
@@ -280,85 +313,219 @@ public class WifiVendorHal {
* @return true for success
*/
public boolean startVendorHalSta() {
- return startVendorHal(STA_MODE);
+ synchronized (sLock) {
+ if (!startVendorHal()) {
+ return false;
+ }
+ if (TextUtils.isEmpty(createStaIface(null))) {
+ stopVendorHal();
+ return false;
+ }
+ return true;
+ }
}
- public static final boolean STA_MODE = true;
- public static final boolean AP_MODE = false;
+ /**
+ * Bring up the HIDL Vendor HAL.
+ * @return true on success, false otherwise.
+ */
+ public boolean startVendorHal() {
+ synchronized (sLock) {
+ if (!mHalDeviceManager.start()) {
+ mLog.err("Failed to start vendor HAL");
+ return false;
+ }
+ mLog.i("Vendor Hal started successfully");
+ return true;
+ }
+ }
+
+ /** Helper method to lookup the corresponding STA iface object using iface name. */
+ private IWifiStaIface getStaIface(@NonNull String ifaceName) {
+ synchronized (sLock) {
+ return mIWifiStaIfaces.get(ifaceName);
+ }
+ }
+
+ private class StaInterfaceDestroyedListenerInternal implements InterfaceDestroyedListener {
+ private final InterfaceDestroyedListener mExternalListener;
+
+ StaInterfaceDestroyedListenerInternal(InterfaceDestroyedListener externalListener) {
+ mExternalListener = externalListener;
+ }
+
+ @Override
+ public void onDestroyed(@NonNull String ifaceName) {
+ synchronized (sLock) {
+ mIWifiStaIfaces.remove(ifaceName);
+ }
+ if (mExternalListener != null) {
+ mExternalListener.onDestroyed(ifaceName);
+ }
+ }
+ }
/**
- * Bring up the HIDL Vendor HAL and configure for STA mode or AP mode.
+ * Create a STA iface using {@link HalDeviceManager}.
*
- * @param isStaMode true to start HAL in STA mode, false to start in AP mode.
+ * @param destroyedListener Listener to be invoked when the interface is destroyed.
+ * @return iface name on success, null otherwise.
*/
- public boolean startVendorHal(boolean isStaMode) {
+ public String createStaIface(InterfaceDestroyedListener destroyedListener) {
synchronized (sLock) {
- if (mIWifiStaIface != null) return boolResult(false);
- if (mIWifiApIface != null) return boolResult(false);
- if (!mHalDeviceManager.start()) {
- return startFailedTo("start the vendor HAL");
+ IWifiStaIface iface = mHalDeviceManager.createStaIface(
+ new StaInterfaceDestroyedListenerInternal(destroyedListener), null);
+ if (iface == null) {
+ mLog.err("Failed to create STA iface");
+ return stringResult(null);
}
- IWifiIface iface;
- if (isStaMode) {
- mIWifiStaIface = mHalDeviceManager.createStaIface(null, null);
- if (mIWifiStaIface == null) {
- return startFailedTo("create STA Iface");
- }
- iface = (IWifiIface) mIWifiStaIface;
- if (!registerStaIfaceCallback()) {
- return startFailedTo("register sta iface callback");
- }
- mIWifiRttController = mHalDeviceManager.createRttController();
- if (mIWifiRttController == null) {
- return startFailedTo("create RTT controller");
- }
- if (!registerRttEventCallback()) {
- return startFailedTo("register RTT iface callback");
- }
- enableLinkLayerStats();
- } else {
- mIWifiApIface = mHalDeviceManager.createApIface(null, null);
- if (mIWifiApIface == null) {
- return startFailedTo("create AP Iface");
- }
- iface = (IWifiIface) mIWifiApIface;
+ String ifaceName = mHalDeviceManager.getName((IWifiIface) iface);
+ if (TextUtils.isEmpty(ifaceName)) {
+ mLog.err("Failed to get iface name");
+ return stringResult(null);
}
- mIWifiChip = mHalDeviceManager.getChip(iface);
- if (mIWifiChip == null) {
- return startFailedTo("get the chip created for the Iface");
+ if (!registerStaIfaceCallback(iface)) {
+ mLog.err("Failed to register STA iface callback");
+ return stringResult(null);
}
- if (!registerChipCallback()) {
- return startFailedTo("register chip callback");
+ mIWifiRttController = mHalDeviceManager.createRttController();
+ if (mIWifiRttController == null) {
+ mLog.err("Failed to create RTT controller");
+ return stringResult(null);
}
- mLog.i("Vendor Hal started successfully");
+ if (!registerRttEventCallback()) {
+ mLog.err("Failed to register RTT controller callback");
+ return stringResult(null);
+ }
+ if (!retrieveWifiChip((IWifiIface) iface)) {
+ mLog.err("Failed to get wifi chip");
+ return stringResult(null);
+ }
+ enableLinkLayerStats(iface);
+ mIWifiStaIfaces.put(ifaceName, iface);
+ return ifaceName;
+ }
+ }
+
+ /**
+ * Remove a STA iface using {@link HalDeviceManager}.
+ *
+ * @param ifaceName Name of the interface being removed.
+ * @return true on success, false otherwise.
+ */
+ public boolean removeStaIface(@NonNull String ifaceName) {
+ synchronized (sLock) {
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
+
+ if (!mHalDeviceManager.removeIface((IWifiIface) iface)) {
+ mLog.err("Failed to remove STA iface");
+ return boolResult(false);
+ }
+ mIWifiStaIfaces.remove(ifaceName);
return true;
}
}
+ /** Helper method to lookup the corresponding AP iface object using iface name. */
+ private IWifiApIface getApIface(@NonNull String ifaceName) {
+ synchronized (sLock) {
+ return mIWifiApIfaces.get(ifaceName);
+ }
+ }
+
+ private class ApInterfaceDestroyedListenerInternal implements InterfaceDestroyedListener {
+ private final InterfaceDestroyedListener mExternalListener;
+
+ ApInterfaceDestroyedListenerInternal(InterfaceDestroyedListener externalListener) {
+ mExternalListener = externalListener;
+ }
+
+ @Override
+ public void onDestroyed(@NonNull String ifaceName) {
+ synchronized (sLock) {
+ mIWifiApIfaces.remove(ifaceName);
+ }
+ if (mExternalListener != null) {
+ mExternalListener.onDestroyed(ifaceName);
+ }
+ }
+ }
+
+
/**
- * Logs a message and cleans up after a failing start attempt
+ * Create a AP iface using {@link HalDeviceManager}.
*
- * The lock should be held.
- * @param message describes what was being attempted
- * @return false
+ * @param destroyedListener Listener to be invoked when the interface is destroyed.
+ * @return iface name on success, null otherwise.
*/
- private boolean startFailedTo(String message) {
- mVerboseLog.err("Failed to %. Vendor Hal start failed").c(message).flush();
- mHalDeviceManager.stop();
- clearState();
- return false;
+ public String createApIface(InterfaceDestroyedListener destroyedListener) {
+ synchronized (sLock) {
+ IWifiApIface iface = mHalDeviceManager.createApIface(
+ new ApInterfaceDestroyedListenerInternal(destroyedListener), null);
+ if (iface == null) {
+ mLog.err("Failed to create AP iface");
+ return stringResult(null);
+ }
+ String ifaceName = mHalDeviceManager.getName((IWifiIface) iface);
+ if (TextUtils.isEmpty(ifaceName)) {
+ mLog.err("Failed to get iface name");
+ return stringResult(null);
+ }
+ if (!retrieveWifiChip((IWifiIface) iface)) {
+ mLog.err("Failed to get wifi chip");
+ return stringResult(null);
+ }
+ mIWifiApIfaces.put(ifaceName, iface);
+ return ifaceName;
+ }
+ }
+
+ /**
+ * Remove an AP iface using {@link HalDeviceManager}.
+ *
+ * @param ifaceName Name of the interface being removed.
+ * @return true on success, false otherwise.
+ */
+ public boolean removeApIface(@NonNull String ifaceName) {
+ synchronized (sLock) {
+ IWifiApIface iface = getApIface(ifaceName);
+ if (iface == null) return boolResult(false);
+
+ if (!mHalDeviceManager.removeIface((IWifiIface) iface)) {
+ mLog.err("Failed to remove AP iface");
+ return boolResult(false);
+ }
+ mIWifiApIfaces.remove(ifaceName);
+ return true;
+ }
+ }
+
+ private boolean retrieveWifiChip(IWifiIface iface) {
+ synchronized (sLock) {
+ mIWifiChip = mHalDeviceManager.getChip(iface);
+ if (mIWifiChip == null) {
+ mLog.err("Failed to get the chip created for the Iface");
+ return false;
+ }
+ if (!registerChipCallback()) {
+ mLog.err("Failed to register chip callback");
+ return false;
+ }
+ return true;
+ }
}
/**
* Registers the sta iface callback.
*/
- private boolean registerStaIfaceCallback() {
+ private boolean registerStaIfaceCallback(IWifiStaIface iface) {
synchronized (sLock) {
- if (mIWifiStaIface == null) return boolResult(false);
+ if (iface == null) return boolResult(false);
if (mIWifiStaIfaceEventCallback == null) return boolResult(false);
try {
WifiStatus status =
- mIWifiStaIface.registerEventCallback(mIWifiStaIfaceEventCallback);
+ iface.registerEventCallback(mIWifiStaIfaceEventCallback);
return ok(status);
} catch (RemoteException e) {
handleRemoteException(e);
@@ -390,6 +557,7 @@ public class WifiVendorHal {
private boolean registerRttEventCallback() {
synchronized (sLock) {
if (mIWifiRttController == null) return boolResult(false);
+ if (mRttEventCallback == null) return boolResult(false);
try {
WifiStatus status = mIWifiRttController.registerEventCallback(mRttEventCallback);
return ok(status);
@@ -418,36 +586,39 @@ public class WifiVendorHal {
*/
private void clearState() {
mIWifiChip = null;
- mIWifiStaIface = null;
- mIWifiApIface = null;
+ mIWifiStaIfaces.clear();
+ mIWifiApIfaces.clear();
mIWifiRttController = null;
mDriverDescription = null;
mFirmwareDescription = null;
}
/**
- * Tests whether the HAL is running or not
+ * Tests whether the HAL is started and atleast one iface is up.
*/
public boolean isHalStarted() {
// For external use only. Methods in this class should test for null directly.
synchronized (sLock) {
- return (mIWifiStaIface != null || mIWifiApIface != null);
+ return (!mIWifiStaIfaces.isEmpty() || !mIWifiApIfaces.isEmpty());
}
}
/**
* Gets the scan capabilities
*
+ * @param ifaceName Name of the interface.
* @param capabilities object to be filled in
* @return true for success, false for failure
*/
- public boolean getBgScanCapabilities(WifiNative.ScanCapabilities capabilities) {
+ public boolean getBgScanCapabilities(
+ @NonNull String ifaceName, WifiNative.ScanCapabilities capabilities) {
synchronized (sLock) {
- if (mIWifiStaIface == null) return boolResult(false);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
try {
MutableBoolean ans = new MutableBoolean(false);
WifiNative.ScanCapabilities out = capabilities;
- mIWifiStaIface.getBackgroundScanCapabilities((status, cap) -> {
+ iface.getBackgroundScanCapabilities((status, cap) -> {
if (!ok(status)) return;
mVerboseLog.info("scan capabilities %").c(cap.toString()).flush();
out.max_scan_cache_size = cap.maxCacheSize;
@@ -582,24 +753,27 @@ public class WifiVendorHal {
*
* Any ongoing scan will be stopped first
*
+ * @param ifaceName Name of the interface.
* @param settings to control the scan
* @param eventHandler to call with the results
* @return true for success
*/
- public boolean startBgScan(WifiNative.ScanSettings settings,
+ public boolean startBgScan(@NonNull String ifaceName,
+ WifiNative.ScanSettings settings,
WifiNative.ScanEventHandler eventHandler) {
WifiStatus status;
if (eventHandler == null) return boolResult(false);
synchronized (sLock) {
- if (mIWifiStaIface == null) return boolResult(false);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
try {
if (mScan != null && !mScan.paused) {
- ok(mIWifiStaIface.stopBackgroundScan(mScan.cmdId));
+ ok(iface.stopBackgroundScan(mScan.cmdId));
mScan = null;
}
mLastScanCmdId = (mLastScanCmdId % 9) + 1; // cycle through non-zero single digits
CurrentBackgroundScan scan = new CurrentBackgroundScan(mLastScanCmdId, settings);
- status = mIWifiStaIface.startBackgroundScan(scan.cmdId, scan.param);
+ status = iface.startBackgroundScan(scan.cmdId, scan.param);
if (!ok(status)) return false;
scan.eventHandler = eventHandler;
mScan = scan;
@@ -614,14 +788,17 @@ public class WifiVendorHal {
/**
* Stops any ongoing backgound scan
+ *
+ * @param ifaceName Name of the interface.
*/
- public void stopBgScan() {
+ public void stopBgScan(@NonNull String ifaceName) {
WifiStatus status;
synchronized (sLock) {
- if (mIWifiStaIface == null) return;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return;
try {
if (mScan != null) {
- ok(mIWifiStaIface.stopBackgroundScan(mScan.cmdId));
+ ok(iface.stopBackgroundScan(mScan.cmdId));
mScan = null;
}
} catch (RemoteException e) {
@@ -632,14 +809,17 @@ public class WifiVendorHal {
/**
* Pauses an ongoing backgound scan
+ *
+ * @param ifaceName Name of the interface.
*/
- public void pauseBgScan() {
+ public void pauseBgScan(@NonNull String ifaceName) {
WifiStatus status;
synchronized (sLock) {
try {
- if (mIWifiStaIface == null) return;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return;
if (mScan != null && !mScan.paused) {
- status = mIWifiStaIface.stopBackgroundScan(mScan.cmdId);
+ status = iface.stopBackgroundScan(mScan.cmdId);
if (!ok(status)) return;
mScan.paused = true;
}
@@ -651,14 +831,17 @@ public class WifiVendorHal {
/**
* Restarts a paused background scan
+ *
+ * @param ifaceName Name of the interface.
*/
- public void restartBgScan() {
+ public void restartBgScan(@NonNull String ifaceName) {
WifiStatus status;
synchronized (sLock) {
- if (mIWifiStaIface == null) return;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return;
try {
if (mScan != null && mScan.paused) {
- status = mIWifiStaIface.startBackgroundScan(mScan.cmdId, mScan.param);
+ status = iface.startBackgroundScan(mScan.cmdId, mScan.param);
if (!ok(status)) return;
mScan.paused = false;
}
@@ -672,10 +855,13 @@ public class WifiVendorHal {
* Gets the latest scan results received from the HIDL interface callback.
* TODO(b/35754840): This hop to fetch scan results after callback is unnecessary. Refactor
* WifiScanner to use the scan results from the callback.
+ *
+ * @param ifaceName Name of the interface.
*/
- public WifiScanner.ScanData[] getBgScanResults() {
+ public WifiScanner.ScanData[] getBgScanResults(@NonNull String ifaceName) {
synchronized (sLock) {
- if (mIWifiStaIface == null) return null;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return null;
if (mScan == null) return null;
return mScan.latestScanResults;
}
@@ -686,17 +872,19 @@ public class WifiVendorHal {
*
* Note - we always enable link layer stats on a STA interface.
*
+ * @param ifaceName Name of the interface.
* @return the statistics, or null if unable to do so
*/
- public WifiLinkLayerStats getWifiLinkLayerStats() {
+ public WifiLinkLayerStats getWifiLinkLayerStats(@NonNull String ifaceName) {
class AnswerBox {
public StaLinkLayerStats value = null;
}
AnswerBox answer = new AnswerBox();
synchronized (sLock) {
try {
- if (mIWifiStaIface == null) return null;
- mIWifiStaIface.getLinkLayerStats((status, stats) -> {
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return null;
+ iface.getLinkLayerStats((status, stats) -> {
if (!ok(status)) return;
answer.value = stats;
});
@@ -716,7 +904,6 @@ public class WifiVendorHal {
static WifiLinkLayerStats frameworkFromHalLinkLayerStats(StaLinkLayerStats stats) {
if (stats == null) return null;
WifiLinkLayerStats out = new WifiLinkLayerStats();
- // unpopulated: out.status, out.SSID, out.BSSID
out.beacon_rx = stats.iface.beaconRx;
out.rssi_mgmt = stats.iface.avgRssiMgmt;
// Statistics are broken out by Wireless Multimedia Extensions categories
@@ -752,7 +939,7 @@ public class WifiVendorHal {
out.rx_time = radioStats.rxTimeInMs;
out.on_time_scan = radioStats.onTimeInMsForScan;
}
- // unused: stats.timeStampInMs;
+ out.timeStampInMs = stats.timeStampInMs;
return out;
}
@@ -763,12 +950,14 @@ public class WifiVendorHal {
* Enables the linkLayerStats in the Hal.
*
* This is called unconditionally whenever we create a STA interface.
+ *
+ * @param iface Iface object.
*/
- private void enableLinkLayerStats() {
+ private void enableLinkLayerStats(IWifiStaIface iface) {
synchronized (sLock) {
try {
WifiStatus status;
- status = mIWifiStaIface.enableLinkLayerStatsCollection(mLinkLayerStatsDebug);
+ status = iface.enableLinkLayerStatsCollection(mLinkLayerStatsDebug);
if (!ok(status)) {
mLog.e("unable to enable link layer stats collection");
}
@@ -877,9 +1066,10 @@ public class WifiVendorHal {
*
* The result may differ depending on the mode (STA or AP)
*
+ * @param ifaceName Name of the interface.
* @return bitmask defined by WifiManager.WIFI_FEATURE_*
*/
- public int getSupportedFeatureSet() {
+ public int getSupportedFeatureSet(@NonNull String ifaceName) {
int featureSet = 0;
if (!mHalDeviceManager.isStarted()) {
return featureSet; // TODO: can't get capabilities with Wi-Fi down
@@ -893,8 +1083,9 @@ public class WifiVendorHal {
feat.value = wifiFeatureMaskFromChipCapabilities(capabilities);
});
}
- if (mIWifiStaIface != null) {
- mIWifiStaIface.getCapabilities((status, capabilities) -> {
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface != null) {
+ iface.getCapabilities((status, capabilities) -> {
if (!ok(status)) return;
feat.value |= wifiFeatureMaskFromStaCapabilities(capabilities);
});
@@ -1433,16 +1624,18 @@ public class WifiVendorHal {
* An OUI {Organizationally Unique Identifier} is a 24-bit number that
* uniquely identifies a vendor or manufacturer.
*
+ * @param ifaceName Name of the interface.
* @param oui
* @return true for success
*/
- public boolean setScanningMacOui(byte[] oui) {
+ public boolean setScanningMacOui(@NonNull String ifaceName, byte[] oui) {
if (oui == null) return boolResult(false);
if (oui.length != 3) return boolResult(false);
synchronized (sLock) {
try {
- if (mIWifiStaIface == null) return boolResult(false);
- WifiStatus status = mIWifiStaIface.setScanningMacOui(oui);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
+ WifiStatus status = iface.setScanningMacOui(oui);
if (!ok(status)) return false;
return true;
} catch (RemoteException e) {
@@ -1454,16 +1647,20 @@ public class WifiVendorHal {
/**
* Get the APF (Android Packet Filter) capabilities of the device
+ *
+ * @param ifaceName Name of the interface.
+ * @return APF capabilities object.
*/
- public ApfCapabilities getApfCapabilities() {
+ public ApfCapabilities getApfCapabilities(@NonNull String ifaceName) {
class AnswerBox {
public ApfCapabilities value = sNoApfCapabilities;
}
synchronized (sLock) {
try {
- if (mIWifiStaIface == null) return sNoApfCapabilities;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return sNoApfCapabilities;
AnswerBox box = new AnswerBox();
- mIWifiStaIface.getApfPacketFilterCapabilities((status, capabilities) -> {
+ iface.getApfPacketFilterCapabilities((status, capabilities) -> {
if (!ok(status)) return;
box.value = new ApfCapabilities(
/* apfVersionSupported */ capabilities.version,
@@ -1483,10 +1680,11 @@ public class WifiVendorHal {
/**
* Installs an APF program on this iface, replacing any existing program.
*
+ * @param ifaceName Name of the interface.
* @param filter is the android packet filter program
* @return true for success
*/
- public boolean installPacketFilter(byte[] filter) {
+ public boolean installPacketFilter(@NonNull String ifaceName, byte[] filter) {
int cmdId = 0; // We only aspire to support one program at a time
if (filter == null) return boolResult(false);
// Copy the program before taking the lock.
@@ -1494,8 +1692,9 @@ public class WifiVendorHal {
enter("filter length %").c(filter.length).flush();
synchronized (sLock) {
try {
- if (mIWifiStaIface == null) return boolResult(false);
- WifiStatus status = mIWifiStaIface.installApfPacketFilter(cmdId, program);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
+ WifiStatus status = iface.installApfPacketFilter(cmdId, program);
if (!ok(status)) return false;
return true;
} catch (RemoteException e) {
@@ -1508,10 +1707,11 @@ public class WifiVendorHal {
/**
* Set country code for this AP iface.
*
+ * @param ifaceName Name of the interface.
* @param countryCode - two-letter country code (as ISO 3166)
* @return true for success
*/
- public boolean setCountryCodeHal(String countryCode) {
+ public boolean setCountryCodeHal(@NonNull String ifaceName, String countryCode) {
if (countryCode == null) return boolResult(false);
if (countryCode.length() != 2) return boolResult(false);
byte[] code;
@@ -1522,8 +1722,9 @@ public class WifiVendorHal {
}
synchronized (sLock) {
try {
- if (mIWifiApIface == null) return boolResult(false);
- WifiStatus status = mIWifiApIface.setCountryCode(code);
+ IWifiApIface iface = getApIface(ifaceName);
+ if (iface == null) return boolResult(false);
+ WifiStatus status = iface.setCountryCode(code);
if (!ok(status)) return false;
return true;
} catch (RemoteException e) {
@@ -1809,13 +2010,15 @@ public class WifiVendorHal {
* <p>
* Once started, monitoring remains active until HAL is unloaded.
*
+ * @param ifaceName Name of the interface.
* @return true for success
*/
- public boolean startPktFateMonitoring() {
+ public boolean startPktFateMonitoring(@NonNull String ifaceName) {
synchronized (sLock) {
- if (mIWifiStaIface == null) return boolResult(false);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
try {
- WifiStatus status = mIWifiStaIface.startDebugPacketFateMonitoring();
+ WifiStatus status = iface.startDebugPacketFateMonitoring();
return ok(status);
} catch (RemoteException e) {
handleRemoteException(e);
@@ -1898,16 +2101,18 @@ public class WifiVendorHal {
* <p>
* Reports the outbound frames for the most recent association (space allowing).
*
+ * @param ifaceName Name of the interface.
* @param reportBufs
* @return true for success
*/
- public boolean getTxPktFates(WifiNative.TxFateReport[] reportBufs) {
+ public boolean getTxPktFates(@NonNull String ifaceName, WifiNative.TxFateReport[] reportBufs) {
if (ArrayUtils.isEmpty(reportBufs)) return boolResult(false);
synchronized (sLock) {
- if (mIWifiStaIface == null) return boolResult(false);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
try {
MutableBoolean ok = new MutableBoolean(false);
- mIWifiStaIface.getDebugTxPacketFates((status, fates) -> {
+ iface.getDebugTxPacketFates((status, fates) -> {
if (!ok(status)) return;
int i = 0;
for (WifiDebugTxPacketFateReport fate : fates) {
@@ -1938,16 +2143,18 @@ public class WifiVendorHal {
* <p>
* Reports the inbound frames for the most recent association (space allowing).
*
+ * @param ifaceName Name of the interface.
* @param reportBufs
* @return true for success
*/
- public boolean getRxPktFates(WifiNative.RxFateReport[] reportBufs) {
+ public boolean getRxPktFates(@NonNull String ifaceName, WifiNative.RxFateReport[] reportBufs) {
if (ArrayUtils.isEmpty(reportBufs)) return boolResult(false);
synchronized (sLock) {
- if (mIWifiStaIface == null) return boolResult(false);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
try {
MutableBoolean ok = new MutableBoolean(false);
- mIWifiStaIface.getDebugRxPacketFates((status, fates) -> {
+ iface.getDebugRxPacketFates((status, fates) -> {
if (!ok(status)) return;
int i = 0;
for (WifiDebugRxPacketFateReport fate : fates) {
@@ -1976,6 +2183,7 @@ public class WifiVendorHal {
/**
* Start sending the specified keep alive packets periodically.
*
+ * @param ifaceName Name of the interface.
* @param slot
* @param srcMac
* @param keepAlivePacket
@@ -1983,16 +2191,18 @@ public class WifiVendorHal {
* @return 0 for success, -1 for error
*/
public int startSendingOffloadedPacket(
- int slot, byte[] srcMac, KeepalivePacketData keepAlivePacket, int periodInMs) {
+ @NonNull String ifaceName, int slot, byte[] srcMac,
+ KeepalivePacketData keepAlivePacket, int periodInMs) {
enter("slot=% periodInMs=%").c(slot).c(periodInMs).flush();
ArrayList<Byte> data = NativeUtil.byteArrayToArrayList(keepAlivePacket.data);
short protocol = (short) (keepAlivePacket.protocol);
synchronized (sLock) {
- if (mIWifiStaIface == null) return -1;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return -1;
try {
- WifiStatus status = mIWifiStaIface.startSendingKeepAlivePackets(
+ WifiStatus status = iface.startSendingKeepAlivePackets(
slot,
data,
protocol,
@@ -2011,16 +2221,18 @@ public class WifiVendorHal {
/**
* Stop sending the specified keep alive packets.
*
+ * @param ifaceName Name of the interface.
* @param slot id - same as startSendingOffloadedPacket call.
* @return 0 for success, -1 for error
*/
- public int stopSendingOffloadedPacket(int slot) {
+ public int stopSendingOffloadedPacket(@NonNull String ifaceName, int slot) {
enter("slot=%").c(slot).flush();
synchronized (sLock) {
- if (mIWifiStaIface == null) return -1;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return -1;
try {
- WifiStatus status = mIWifiStaIface.stopSendingKeepAlivePackets(slot);
+ WifiStatus status = iface.stopSendingKeepAlivePackets(slot);
if (!ok(status)) return -1;
return 0;
} catch (RemoteException e) {
@@ -2044,22 +2256,24 @@ public class WifiVendorHal {
/**
* Start RSSI monitoring on the currently connected access point.
*
+ * @param ifaceName Name of the interface.
* @param maxRssi Maximum RSSI threshold.
* @param minRssi Minimum RSSI threshold.
* @param rssiEventHandler Called when RSSI goes above maxRssi or below minRssi
* @return 0 for success, -1 for failure
*/
- public int startRssiMonitoring(byte maxRssi, byte minRssi,
+ public int startRssiMonitoring(@NonNull String ifaceName, byte maxRssi, byte minRssi,
WifiNative.WifiRssiEventHandler rssiEventHandler) {
enter("maxRssi=% minRssi=%").c(maxRssi).c(minRssi).flush();
if (maxRssi <= minRssi) return -1;
if (rssiEventHandler == null) return -1;
synchronized (sLock) {
- if (mIWifiStaIface == null) return -1;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return -1;
try {
- mIWifiStaIface.stopRssiMonitoring(sRssiMonCmdId);
+ iface.stopRssiMonitoring(sRssiMonCmdId);
WifiStatus status;
- status = mIWifiStaIface.startRssiMonitoring(sRssiMonCmdId, maxRssi, minRssi);
+ status = iface.startRssiMonitoring(sRssiMonCmdId, maxRssi, minRssi);
if (!ok(status)) return -1;
mWifiRssiEventHandler = rssiEventHandler;
return 0;
@@ -2073,15 +2287,16 @@ public class WifiVendorHal {
/**
* Stop RSSI monitoring
*
+ * @param ifaceName Name of the interface.
* @return 0 for success, -1 for failure
*/
- public int stopRssiMonitoring() {
+ public int stopRssiMonitoring(@NonNull String ifaceName) {
synchronized (sLock) {
mWifiRssiEventHandler = null;
- if (mIWifiStaIface == null) return -1;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return -1;
try {
- mIWifiStaIface.stopRssiMonitoring(sRssiMonCmdId);
- WifiStatus status = mIWifiStaIface.stopRssiMonitoring(sRssiMonCmdId);
+ WifiStatus status = iface.stopRssiMonitoring(sRssiMonCmdId);
if (!ok(status)) return -1;
return 0;
} catch (RemoteException e) {
@@ -2158,14 +2373,17 @@ public class WifiVendorHal {
/**
* Enable/Disable Neighbour discovery offload functionality in the firmware.
*
+ * @param ifaceName Name of the interface.
* @param enabled true to enable, false to disable.
+ * @return true for success, false for failure
*/
- public boolean configureNeighborDiscoveryOffload(boolean enabled) {
+ public boolean configureNeighborDiscoveryOffload(@NonNull String ifaceName, boolean enabled) {
enter("enabled=%").c(enabled).flush();
synchronized (sLock) {
- if (mIWifiStaIface == null) return boolResult(false);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
try {
- WifiStatus status = mIWifiStaIface.enableNdOffload(enabled);
+ WifiStatus status = iface.enableNdOffload(enabled);
if (!ok(status)) return false;
} catch (RemoteException e) {
handleRemoteException(e);
@@ -2180,16 +2398,19 @@ public class WifiVendorHal {
/**
* Query the firmware roaming capabilities.
*
+ * @param ifaceName Name of the interface.
* @param capabilities object to be filled in
* @return true for success; false for failure
*/
- public boolean getRoamingCapabilities(WifiNative.RoamingCapabilities capabilities) {
+ public boolean getRoamingCapabilities(@NonNull String ifaceName,
+ WifiNative.RoamingCapabilities capabilities) {
synchronized (sLock) {
- if (mIWifiStaIface == null) return boolResult(false);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
try {
MutableBoolean ok = new MutableBoolean(false);
WifiNative.RoamingCapabilities out = capabilities;
- mIWifiStaIface.getRoamingCapabilities((status, cap) -> {
+ iface.getRoamingCapabilities((status, cap) -> {
if (!ok(status)) return;
out.maxBlacklistSize = cap.maxBlacklistSize;
out.maxWhitelistSize = cap.maxWhitelistSize;
@@ -2206,12 +2427,14 @@ public class WifiVendorHal {
/**
* Enable/disable firmware roaming.
*
+ * @param ifaceName Name of the interface.
* @param state the intended roaming state
* @return SUCCESS, FAILURE, or BUSY
*/
- public int enableFirmwareRoaming(int state) {
+ public int enableFirmwareRoaming(@NonNull String ifaceName, int state) {
synchronized (sLock) {
- if (mIWifiStaIface == null) return WifiStatusCode.ERROR_NOT_STARTED;
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return WifiStatusCode.ERROR_NOT_STARTED;
try {
byte val;
switch (state) {
@@ -2226,7 +2449,7 @@ public class WifiVendorHal {
return WifiStatusCode.ERROR_INVALID_ARGS;
}
- WifiStatus status = mIWifiStaIface.setRoamingState(val);
+ WifiStatus status = iface.setRoamingState(val);
mVerboseLog.d("setRoamingState returned " + status.code);
return status.code;
} catch (RemoteException e) {
@@ -2239,12 +2462,14 @@ public class WifiVendorHal {
/**
* Set firmware roaming configurations.
*
+ * @param ifaceName Name of the interface.
* @param config new roaming configuration object
* @return true for success; false for failure
*/
- public boolean configureRoaming(WifiNative.RoamingConfig config) {
+ public boolean configureRoaming(@NonNull String ifaceName, WifiNative.RoamingConfig config) {
synchronized (sLock) {
- if (mIWifiStaIface == null) return boolResult(false);
+ IWifiStaIface iface = getStaIface(ifaceName);
+ if (iface == null) return boolResult(false);
try {
StaRoamingConfig roamingConfig = new StaRoamingConfig();
@@ -2275,7 +2500,7 @@ public class WifiVendorHal {
}
}
- WifiStatus status = mIWifiStaIface.configureRoaming(roamingConfig);
+ WifiStatus status = iface.configureRoaming(roamingConfig);
if (!ok(status)) return false;
} catch (RemoteException e) {
handleRemoteException(e);
diff --git a/com/android/server/wifi/WificondControl.java b/com/android/server/wifi/WificondControl.java
index 3dd63114..68da98fb 100644
--- a/com/android/server/wifi/WificondControl.java
+++ b/com/android/server/wifi/WificondControl.java
@@ -29,6 +29,7 @@ import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiScanner;
import android.net.wifi.WifiSsid;
import android.os.Binder;
+import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
@@ -46,13 +47,15 @@ import com.android.server.wifi.wificond.SingleScanSettings;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Set;
/**
* This class provides methods for WifiNative to send control commands to wificond.
* NOTE: This class should only be used from WifiNative.
*/
-public class WificondControl {
+public class WificondControl implements IBinder.DeathRecipient {
private boolean mVerboseLoggingEnabled = false;
private static final String TAG = "WificondControl";
@@ -69,27 +72,31 @@ public class WificondControl {
// Cached wificond binder handlers.
private IWificond mWificond;
- private IClientInterface mClientInterface;
- private IApInterface mApInterface;
- private IWifiScannerImpl mWificondScanner;
- private IScanEvent mScanEventHandler;
- private IPnoScanEvent mPnoScanEventHandler;
- private IApInterfaceEventCallback mApInterfaceListener;
+ private HashMap<String, IClientInterface> mClientInterfaces = new HashMap<>();
+ private HashMap<String, IApInterface> mApInterfaces = new HashMap<>();
+ private HashMap<String, IWifiScannerImpl> mWificondScanners = new HashMap<>();
+ private HashMap<String, IScanEvent> mScanEventHandlers = new HashMap<>();
+ private HashMap<String, IPnoScanEvent> mPnoScanEventHandlers = new HashMap<>();
+ private HashMap<String, IApInterfaceEventCallback> mApInterfaceListeners = new HashMap<>();
+ private WifiNative.WificondDeathEventHandler mDeathEventHandler;
- private String mClientInterfaceName;
+ private class ScanEventHandler extends IScanEvent.Stub {
+ private String mIfaceName;
+ ScanEventHandler(@NonNull String ifaceName) {
+ mIfaceName = ifaceName;
+ }
- private class ScanEventHandler extends IScanEvent.Stub {
@Override
public void OnScanResultReady() {
Log.d(TAG, "Scan result ready event");
- mWifiMonitor.broadcastScanResultEvent(mClientInterfaceName);
+ mWifiMonitor.broadcastScanResultEvent(mIfaceName);
}
@Override
public void OnScanFailed() {
Log.d(TAG, "Scan failed event");
- mWifiMonitor.broadcastScanFailedEvent(mClientInterfaceName);
+ mWifiMonitor.broadcastScanFailedEvent(mIfaceName);
}
}
@@ -101,10 +108,16 @@ public class WificondControl {
}
private class PnoScanEventHandler extends IPnoScanEvent.Stub {
+ private String mIfaceName;
+
+ PnoScanEventHandler(@NonNull String ifaceName) {
+ mIfaceName = ifaceName;
+ }
+
@Override
public void OnPnoNetworkFound() {
Log.d(TAG, "Pno scan result event");
- mWifiMonitor.broadcastPnoScanResultEvent(mClientInterfaceName);
+ mWifiMonitor.broadcastPnoScanResultEvent(mIfaceName);
mWifiInjector.getWifiMetrics().incrementPnoFoundNetworkEventCount();
}
@@ -143,6 +156,22 @@ public class WificondControl {
}
}
+ /**
+ * Called by the binder subsystem upon remote object death.
+ * Invoke all the register death handlers and clear state.
+ */
+ @Override
+ public void binderDied() {
+ Log.e(TAG, "Wificond died!");
+ if (mDeathEventHandler != null) {
+ mDeathEventHandler.onDeath();
+ }
+ clearState();
+ // Invalidate the global wificond handle on death. Will be refereshed
+ // on the next setup call.
+ mWificond = null;
+ }
+
/** Enable or disable verbose logging of WificondControl.
* @param enable True to enable verbose logging. False to disable verbose logging.
*/
@@ -151,15 +180,64 @@ public class WificondControl {
}
/**
- * Setup driver for client mode via wificond.
- * @return An IClientInterface as wificond client interface binder handler.
- * Returns null on failure.
- */
- public IClientInterface setupDriverForClientMode(@NonNull String ifaceName) {
- Log.d(TAG, "Setting up driver for client mode");
+ * Registers a death notification for wificond.
+ * @return Returns true on success.
+ */
+ public boolean registerDeathHandler(@NonNull WifiNative.WificondDeathEventHandler handler) {
+ if (mDeathEventHandler != null) {
+ Log.e(TAG, "Death handler already present");
+ return false;
+ }
+ mDeathEventHandler = handler;
+ return true;
+ }
+
+ /**
+ * Deregisters a death notification for wificond.
+ * @return Returns true on success.
+ */
+ public boolean deregisterDeathHandler() {
+ if (mDeathEventHandler == null) {
+ Log.e(TAG, "No Death handler present");
+ return false;
+ }
+ mDeathEventHandler = null;
+ return true;
+ }
+
+ /**
+ * Helper method to retrieve the global wificond handle and register for
+ * death notifications.
+ */
+ private boolean retrieveWificondAndRegisterForDeath() {
+ if (mWificond != null) {
+ Log.d(TAG, "Wificond handle already retrieved");
+ // We already have a wificond handle.
+ return true;
+ }
mWificond = mWifiInjector.makeWificond();
if (mWificond == null) {
Log.e(TAG, "Failed to get reference to wificond");
+ return false;
+ }
+ try {
+ mWificond.asBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to register death notification for wificond");
+ // The remote has already died.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Setup interface for client mode via wificond.
+ * @return An IClientInterface as wificond client interface binder handler.
+ * Returns null on failure.
+ */
+ public IClientInterface setupInterfaceForClientMode(@NonNull String ifaceName) {
+ Log.d(TAG, "Setting up interface for client mode");
+ if (!retrieveWificondAndRegisterForDeath()) {
return null;
}
@@ -178,19 +256,21 @@ public class WificondControl {
Binder.allowBlocking(clientInterface.asBinder());
// Refresh Handlers
- mClientInterface = clientInterface;
+ mClientInterfaces.put(ifaceName, clientInterface);
try {
- mClientInterfaceName = clientInterface.getInterfaceName();
- mWificondScanner = mClientInterface.getWifiScannerImpl();
- if (mWificondScanner == null) {
+ IWifiScannerImpl wificondScanner = clientInterface.getWifiScannerImpl();
+ if (wificondScanner == null) {
Log.e(TAG, "Failed to get WificondScannerImpl");
return null;
}
- Binder.allowBlocking(mWificondScanner.asBinder());
- mScanEventHandler = new ScanEventHandler();
- mWificondScanner.subscribeScanEvents(mScanEventHandler);
- mPnoScanEventHandler = new PnoScanEventHandler();
- mWificondScanner.subscribePnoScanEvents(mPnoScanEventHandler);
+ mWificondScanners.put(ifaceName, wificondScanner);
+ Binder.allowBlocking(wificondScanner.asBinder());
+ ScanEventHandler scanEventHandler = new ScanEventHandler(ifaceName);
+ mScanEventHandlers.put(ifaceName, scanEventHandler);
+ wificondScanner.subscribeScanEvents(scanEventHandler);
+ PnoScanEventHandler pnoScanEventHandler = new PnoScanEventHandler(ifaceName);
+ mPnoScanEventHandlers.put(ifaceName, pnoScanEventHandler);
+ wificondScanner.subscribePnoScanEvents(pnoScanEventHandler);
} catch (RemoteException e) {
Log.e(TAG, "Failed to refresh wificond scanner due to remote exception");
}
@@ -199,15 +279,49 @@ public class WificondControl {
}
/**
- * Setup driver for softAp mode via wificond.
+ * Teardown a specific STA interface configured in wificond.
+ *
+ * @return Returns true on success.
+ */
+ public boolean tearDownClientInterface(@NonNull String ifaceName) {
+ boolean success;
+ try {
+ IWifiScannerImpl scannerImpl = mWificondScanners.get(ifaceName);
+ if (scannerImpl != null) {
+ scannerImpl.unsubscribeScanEvents();
+ scannerImpl.unsubscribePnoScanEvents();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to unsubscribe wificond scanner due to remote exception");
+ return false;
+ }
+
+ try {
+ success = mWificond.tearDownClientInterface(ifaceName);
+ } catch (RemoteException e1) {
+ Log.e(TAG, "Failed to teardown client interface due to remote exception");
+ return false;
+ }
+ if (!success) {
+ Log.e(TAG, "Failed to teardown client interface");
+ return false;
+ }
+
+ mClientInterfaces.remove(ifaceName);
+ mWificondScanners.remove(ifaceName);
+ mScanEventHandlers.remove(ifaceName);
+ mPnoScanEventHandlers.remove(ifaceName);
+ return true;
+ }
+
+ /**
+ * Setup interface for softAp mode via wificond.
* @return An IApInterface as wificond Ap interface binder handler.
* Returns null on failure.
*/
- public IApInterface setupDriverForSoftApMode(@NonNull String ifaceName) {
- Log.d(TAG, "Setting up driver for soft ap mode");
- mWificond = mWifiInjector.makeWificond();
- if (mWificond == null) {
- Log.e(TAG, "Failed to get reference to wificond");
+ public IApInterface setupInterfaceForSoftApMode(@NonNull String ifaceName) {
+ Log.d(TAG, "Setting up interface for soft ap mode");
+ if (!retrieveWificondAndRegisterForDeath()) {
return null;
}
@@ -226,12 +340,33 @@ public class WificondControl {
Binder.allowBlocking(apInterface.asBinder());
// Refresh Handlers
- mApInterface = apInterface;
-
+ mApInterfaces.put(ifaceName, apInterface);
return apInterface;
}
/**
+ * Teardown a specific AP interface configured in wificond.
+ *
+ * @return Returns true on success.
+ */
+ public boolean tearDownSoftApInterface(@NonNull String ifaceName) {
+ boolean success;
+ try {
+ success = mWificond.tearDownApInterface(ifaceName);
+ } catch (RemoteException e1) {
+ Log.e(TAG, "Failed to teardown AP interface due to remote exception");
+ return false;
+ }
+ if (!success) {
+ Log.e(TAG, "Failed to teardown AP interface");
+ return false;
+ }
+ mApInterfaces.remove(ifaceName);
+ mApInterfaceListeners.remove(ifaceName);
+ return true;
+ }
+
+ /**
* Teardown all interfaces configured in wificond.
* @return Returns true on success.
*/
@@ -239,26 +374,17 @@ public class WificondControl {
Log.d(TAG, "tearing down interfaces in wificond");
// Explicitly refresh the wificodn handler because |tearDownInterfaces()|
// could be used to cleanup before we setup any interfaces.
- mWificond = mWifiInjector.makeWificond();
- if (mWificond == null) {
- Log.e(TAG, "Failed to get reference to wificond");
+ if (!retrieveWificondAndRegisterForDeath()) {
return false;
}
try {
- if (mWificondScanner != null) {
- mWificondScanner.unsubscribeScanEvents();
- mWificondScanner.unsubscribePnoScanEvents();
+ for (Map.Entry<String, IWifiScannerImpl> entry : mWificondScanners.entrySet()) {
+ entry.getValue().unsubscribeScanEvents();
+ entry.getValue().unsubscribePnoScanEvents();
}
mWificond.tearDownInterfaces();
-
- // Refresh handlers
- mClientInterface = null;
- mWificondScanner = null;
- mPnoScanEventHandler = null;
- mScanEventHandler = null;
- mApInterface = null;
-
+ clearState();
return true;
} catch (RemoteException e) {
Log.e(TAG, "Failed to tear down interfaces due to remote exception");
@@ -267,13 +393,17 @@ public class WificondControl {
return false;
}
+ /** Helper function to look up the interface handle using name */
+ private IClientInterface getClientInterface(@NonNull String ifaceName) {
+ return mClientInterfaces.get(ifaceName);
+ }
+
/**
* Disable wpa_supplicant via wificond.
* @return Returns true on success.
*/
public boolean disableSupplicant() {
- if (mWificond == null) {
- Log.e(TAG, "No valid handler");
+ if (!retrieveWificondAndRegisterForDeath()) {
return false;
}
try {
@@ -289,11 +419,9 @@ public class WificondControl {
* @return Returns true on success.
*/
public boolean enableSupplicant() {
- if (mWificond == null) {
- Log.e(TAG, "No valid wificond handler");
+ if (!retrieveWificondAndRegisterForDeath()) {
return false;
}
-
try {
return mWificond.enableSupplicant();
} catch (RemoteException e) {
@@ -303,19 +431,21 @@ public class WificondControl {
}
/**
- * Request signal polling to wificond.
- * Returns an SignalPollResult object.
- * Returns null on failure.
- */
- public WifiNative.SignalPollResult signalPoll() {
- if (mClientInterface == null) {
+ * Request signal polling to wificond.
+ * @param ifaceName Name of the interface.
+ * Returns an SignalPollResult object.
+ * Returns null on failure.
+ */
+ public WifiNative.SignalPollResult signalPoll(@NonNull String ifaceName) {
+ IClientInterface iface = getClientInterface(ifaceName);
+ if (iface == null) {
Log.e(TAG, "No valid wificond client interface handler");
return null;
}
int[] resultArray;
try {
- resultArray = mClientInterface.signalPoll();
+ resultArray = iface.signalPoll();
if (resultArray == null || resultArray.length != 3) {
Log.e(TAG, "Invalid signal poll result from wificond");
return null;
@@ -332,19 +462,21 @@ public class WificondControl {
}
/**
- * Fetch TX packet counters on current connection from wificond.
- * Returns an TxPacketCounters object.
- * Returns null on failure.
- */
- public WifiNative.TxPacketCounters getTxPacketCounters() {
- if (mClientInterface == null) {
+ * Fetch TX packet counters on current connection from wificond.
+ * @param ifaceName Name of the interface.
+ * Returns an TxPacketCounters object.
+ * Returns null on failure.
+ */
+ public WifiNative.TxPacketCounters getTxPacketCounters(@NonNull String ifaceName) {
+ IClientInterface iface = getClientInterface(ifaceName);
+ if (iface == null) {
Log.e(TAG, "No valid wificond client interface handler");
return null;
}
int[] resultArray;
try {
- resultArray = mClientInterface.getPacketCounters();
+ resultArray = iface.getPacketCounters();
if (resultArray == null || resultArray.length != 2) {
Log.e(TAG, "Invalid signal poll result from wificond");
return null;
@@ -359,23 +491,30 @@ public class WificondControl {
return counters;
}
+ /** Helper function to look up the scanner impl handle using name */
+ private IWifiScannerImpl getScannerImpl(@NonNull String ifaceName) {
+ return mWificondScanners.get(ifaceName);
+ }
+
/**
* Fetch the latest scan result from kernel via wificond.
+ * @param ifaceName Name of the interface.
* @return Returns an ArrayList of ScanDetail.
* Returns an empty ArrayList on failure.
*/
- public ArrayList<ScanDetail> getScanResults(int scanType) {
+ public ArrayList<ScanDetail> getScanResults(@NonNull String ifaceName, int scanType) {
ArrayList<ScanDetail> results = new ArrayList<>();
- if (mWificondScanner == null) {
+ IWifiScannerImpl scannerImpl = getScannerImpl(ifaceName);
+ if (scannerImpl == null) {
Log.e(TAG, "No valid wificond scanner interface handler");
return results;
}
try {
NativeScanResult[] nativeResults;
if (scanType == SCAN_TYPE_SINGLE_SCAN) {
- nativeResults = mWificondScanner.getScanResults();
+ nativeResults = scannerImpl.getScanResults();
} else {
- nativeResults = mWificondScanner.getPnoScanResults();
+ nativeResults = scannerImpl.getPnoScanResults();
}
for (NativeScanResult result : nativeResults) {
WifiSsid wifiSsid = WifiSsid.createFromByteArray(result.ssid);
@@ -430,12 +569,16 @@ public class WificondControl {
/**
* Start a scan using wificond for the given parameters.
+ * @param ifaceName Name of the interface.
* @param freqs list of frequencies to scan for, if null scan all supported channels.
* @param hiddenNetworkSSIDs List of hidden networks to be scanned for.
* @return Returns true on success.
*/
- public boolean scan(Set<Integer> freqs, Set<String> hiddenNetworkSSIDs) {
- if (mWificondScanner == null) {
+ public boolean scan(@NonNull String ifaceName,
+ Set<Integer> freqs,
+ Set<String> hiddenNetworkSSIDs) {
+ IWifiScannerImpl scannerImpl = getScannerImpl(ifaceName);
+ if (scannerImpl == null) {
Log.e(TAG, "No valid wificond scanner interface handler");
return false;
}
@@ -464,7 +607,7 @@ public class WificondControl {
}
try {
- return mWificondScanner.scan(settings);
+ return scannerImpl.scan(settings);
} catch (RemoteException e1) {
Log.e(TAG, "Failed to request scan due to remote exception");
}
@@ -473,11 +616,13 @@ public class WificondControl {
/**
* Start PNO scan.
+ * @param ifaceName Name of the interface.
* @param pnoSettings Pno scan configuration.
* @return true on success.
*/
- public boolean startPnoScan(WifiNative.PnoSettings pnoSettings) {
- if (mWificondScanner == null) {
+ public boolean startPnoScan(@NonNull String ifaceName, WifiNative.PnoSettings pnoSettings) {
+ IWifiScannerImpl scannerImpl = getScannerImpl(ifaceName);
+ if (scannerImpl == null) {
Log.e(TAG, "No valid wificond scanner interface handler");
return false;
}
@@ -503,7 +648,7 @@ public class WificondControl {
}
try {
- boolean success = mWificondScanner.startPnoScan(settings);
+ boolean success = scannerImpl.startPnoScan(settings);
mWifiInjector.getWifiMetrics().incrementPnoScanStartAttempCount();
if (!success) {
mWifiInjector.getWifiMetrics().incrementPnoScanFailedCount();
@@ -517,15 +662,17 @@ public class WificondControl {
/**
* Stop PNO scan.
+ * @param ifaceName Name of the interface.
* @return true on success.
*/
- public boolean stopPnoScan() {
- if (mWificondScanner == null) {
+ public boolean stopPnoScan(@NonNull String ifaceName) {
+ IWifiScannerImpl scannerImpl = getScannerImpl(ifaceName);
+ if (scannerImpl == null) {
Log.e(TAG, "No valid wificond scanner interface handler");
return false;
}
try {
- return mWificondScanner.stopPnoScan();
+ return scannerImpl.stopPnoScan();
} catch (RemoteException e1) {
Log.e(TAG, "Failed to stop pno scan due to remote exception");
}
@@ -534,14 +681,16 @@ public class WificondControl {
/**
* Abort ongoing single scan.
+ * @param ifaceName Name of the interface.
*/
- public void abortScan() {
- if (mWificondScanner == null) {
+ public void abortScan(@NonNull String ifaceName) {
+ IWifiScannerImpl scannerImpl = getScannerImpl(ifaceName);
+ if (scannerImpl == null) {
Log.e(TAG, "No valid wificond scanner interface handler");
return;
}
try {
- mWificondScanner.abortScan();
+ scannerImpl.abortScan();
} catch (RemoteException e1) {
Log.e(TAG, "Failed to request abortScan due to remote exception");
}
@@ -581,15 +730,24 @@ public class WificondControl {
return null;
}
+ /** Helper function to look up the interface handle using name */
+ private IApInterface getApInterface(@NonNull String ifaceName) {
+ return mApInterfaces.get(ifaceName);
+ }
+
/**
* Start Soft AP operation using the provided configuration.
*
+ * @param ifaceName Name of the interface.
* @param config Configuration to use for the soft ap created.
* @param listener Callback for AP events.
* @return true on success, false otherwise.
*/
- public boolean startSoftAp(WifiConfiguration config, SoftApListener listener) {
- if (mApInterface == null) {
+ public boolean startSoftAp(@NonNull String ifaceName,
+ WifiConfiguration config,
+ SoftApListener listener) {
+ IApInterface iface = getApInterface(ifaceName);
+ if (iface == null) {
Log.e(TAG, "No valid ap interface handler");
return false;
}
@@ -599,7 +757,7 @@ public class WificondControl {
// hex string or "double quoted".
// However, it seems that whatever is handing us these configurations does not obey
// this convention.
- boolean success = mApInterface.writeHostapdConfig(
+ boolean success = iface.writeHostapdConfig(
config.SSID.getBytes(StandardCharsets.UTF_8), config.hiddenSSID,
config.apChannel, encryptionType,
(config.preSharedKey != null)
@@ -609,8 +767,9 @@ public class WificondControl {
Log.e(TAG, "Failed to write hostapd configuration");
return false;
}
- mApInterfaceListener = new ApInterfaceEventCallback(listener);
- success = mApInterface.startHostapd(mApInterfaceListener);
+ IApInterfaceEventCallback callback = new ApInterfaceEventCallback(listener);
+ mApInterfaceListeners.put(ifaceName, callback);
+ success = iface.startHostapd(callback);
if (!success) {
Log.e(TAG, "Failed to start hostapd.");
return false;
@@ -625,15 +784,17 @@ public class WificondControl {
/**
* Stop the ongoing Soft AP operation.
*
+ * @param ifaceName Name of the interface.
* @return true on success, false otherwise.
*/
- public boolean stopSoftAp() {
- if (mApInterface == null) {
+ public boolean stopSoftAp(@NonNull String ifaceName) {
+ IApInterface iface = getApInterface(ifaceName);
+ if (iface == null) {
Log.e(TAG, "No valid ap interface handler");
return false;
}
try {
- boolean success = mApInterface.stopHostapd();
+ boolean success = iface.stopHostapd();
if (!success) {
Log.e(TAG, "Failed to stop hostapd.");
return false;
@@ -642,7 +803,7 @@ public class WificondControl {
Log.e(TAG, "Exception in stopping soft AP: " + e);
return false;
}
- mApInterfaceListener = null;
+ mApInterfaceListeners.remove(ifaceName);
return true;
}
@@ -666,4 +827,17 @@ public class WificondControl {
}
return encryptionType;
}
+
+ /**
+ * Clear all internal handles.
+ */
+ private void clearState() {
+ // Refresh handlers
+ mClientInterfaces.clear();
+ mWificondScanners.clear();
+ mPnoScanEventHandlers.clear();
+ mScanEventHandlers.clear();
+ mApInterfaces.clear();
+ mApInterfaceListeners.clear();
+ }
}
diff --git a/com/android/server/wifi/aware/WifiAwareClientState.java b/com/android/server/wifi/aware/WifiAwareClientState.java
index 987d49ef..d9512578 100644
--- a/com/android/server/wifi/aware/WifiAwareClientState.java
+++ b/com/android/server/wifi/aware/WifiAwareClientState.java
@@ -43,8 +43,8 @@ import java.util.Arrays;
*/
public class WifiAwareClientState {
private static final String TAG = "WifiAwareClientState";
- private static final boolean DBG = false;
private static final boolean VDBG = false; // STOPSHIP if true
+ /* package */ boolean mDbg = false;
/* package */ static final int CLUSTER_CHANGE_EVENT_STARTED = 0;
/* package */ static final int CLUSTER_CHANGE_EVENT_JOINED = 1;
diff --git a/com/android/server/wifi/aware/WifiAwareDataPathStateManager.java b/com/android/server/wifi/aware/WifiAwareDataPathStateManager.java
index 82bc2c43..b2635974 100644
--- a/com/android/server/wifi/aware/WifiAwareDataPathStateManager.java
+++ b/com/android/server/wifi/aware/WifiAwareDataPathStateManager.java
@@ -76,9 +76,8 @@ import java.util.TreeSet;
*/
public class WifiAwareDataPathStateManager {
private static final String TAG = "WifiAwareDataPathStMgr";
-
- private static final boolean DBG = false;
private static final boolean VDBG = false; // STOPSHIP if true
+ /* package */ boolean mDbg = false;
private static final String AWARE_INTERFACE_PREFIX = "aware_data";
private static final String NETWORK_TAG = "WIFI_AWARE_FACTORY";
@@ -204,6 +203,7 @@ public class WifiAwareDataPathStateManager {
String name = AWARE_INTERFACE_PREFIX + i;
mMgr.deleteDataPathInterface(name);
}
+ mMgr.releaseAwareInterface();
}
/**
@@ -350,8 +350,8 @@ public class WifiAwareDataPathStateManager {
if (nnri == null) {
Log.w(TAG, "onDataPathRequest: can't find a request with specified pubSubId=" + pubSubId
+ ", mac=" + String.valueOf(HexEncoding.encode(mac)));
- if (DBG) {
- Log.d(TAG, "onDataPathRequest: network request cache = " + mNetworkRequestsCache);
+ if (VDBG) {
+ Log.v(TAG, "onDataPathRequest: network request cache = " + mNetworkRequestsCache);
}
mMgr.respondToDataPathRequest(false, ndpId, "", null, null, false);
return null;
@@ -408,8 +408,8 @@ public class WifiAwareDataPathStateManager {
if (nnri == null) {
Log.w(TAG, "onRespondToDataPathRequest: can't find a request with specified ndpId="
+ ndpId);
- if (DBG) {
- Log.d(TAG, "onRespondToDataPathRequest: network request cache = "
+ if (VDBG) {
+ Log.v(TAG, "onRespondToDataPathRequest: network request cache = "
+ mNetworkRequestsCache);
}
return;
@@ -531,8 +531,8 @@ public class WifiAwareDataPathStateManager {
nnri.startTimestamp = SystemClock.elapsedRealtime(); // update time-stamp for duration
mAwareMetrics.recordNdpCreation(nnri.uid, mNetworkRequestsCache);
} else {
- if (DBG) {
- Log.d(TAG, "onDataPathConfirm: data-path for networkSpecifier=" + networkSpecifier
+ if (VDBG) {
+ Log.v(TAG, "onDataPathConfirm: data-path for networkSpecifier=" + networkSpecifier
+ " rejected - reason=" + reason);
}
mNetworkRequestsCache.remove(networkSpecifier);
@@ -555,8 +555,8 @@ public class WifiAwareDataPathStateManager {
Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> nnriE =
getNetworkRequestByNdpId(ndpId);
if (nnriE == null) {
- if (DBG) {
- Log.d(TAG, "onDataPathEnd: network request not found for ndpId=" + ndpId);
+ if (VDBG) {
+ Log.v(TAG, "onDataPathEnd: network request not found for ndpId=" + ndpId);
}
return;
}
@@ -590,12 +590,12 @@ public class WifiAwareDataPathStateManager {
* - i.e. when we're proceeding with data-path setup).
*/
public void handleDataPathTimeout(NetworkSpecifier networkSpecifier) {
- if (VDBG) Log.v(TAG, "handleDataPathTimeout: networkSpecifier=" + networkSpecifier);
+ if (mDbg) Log.v(TAG, "handleDataPathTimeout: networkSpecifier=" + networkSpecifier);
AwareNetworkRequestInformation nnri = mNetworkRequestsCache.remove(networkSpecifier);
if (nnri == null) {
- if (DBG) {
- Log.d(TAG,
+ if (mDbg) {
+ Log.v(TAG,
"handleDataPathTimeout: network request not found for networkSpecifier="
+ networkSpecifier);
}
@@ -660,8 +660,8 @@ public class WifiAwareDataPathStateManager {
// look up specifier - are we being called again?
AwareNetworkRequestInformation nnri = mNetworkRequestsCache.get(networkSpecifier);
if (nnri != null) {
- if (DBG) {
- Log.d(TAG, "WifiAwareNetworkFactory.acceptRequest: request=" + request
+ if (VDBG) {
+ Log.v(TAG, "WifiAwareNetworkFactory.acceptRequest: request=" + request
+ " - already in cache with state=" + nnri.state);
}
@@ -728,8 +728,8 @@ public class WifiAwareDataPathStateManager {
}
if (nnri.state != AwareNetworkRequestInformation.STATE_IDLE) {
- if (DBG) {
- Log.d(TAG, "WifiAwareNetworkFactory.needNetworkFor: networkRequest="
+ if (VDBG) {
+ Log.v(TAG, "WifiAwareNetworkFactory.needNetworkFor: networkRequest="
+ networkRequest + " - already in progress");
// TODO: understand how/when can be called again/while in progress (seems
// to be related to score re-calculation after a network agent is created)
diff --git a/com/android/server/wifi/aware/WifiAwareDiscoverySessionState.java b/com/android/server/wifi/aware/WifiAwareDiscoverySessionState.java
index 3358a4a2..062b5d95 100644
--- a/com/android/server/wifi/aware/WifiAwareDiscoverySessionState.java
+++ b/com/android/server/wifi/aware/WifiAwareDiscoverySessionState.java
@@ -38,8 +38,8 @@ import java.util.Arrays;
*/
public class WifiAwareDiscoverySessionState {
private static final String TAG = "WifiAwareDiscSessState";
- private static final boolean DBG = false;
private static final boolean VDBG = false; // STOPSHIP if true
+ /* package */ boolean mDbg = false;
private int mNextPeerIdToBeAllocated = 100; // used to create a unique peer ID
@@ -296,8 +296,8 @@ public class WifiAwareDiscoverySessionState {
PeerInfo newPeerInfo = new PeerInfo(requestorInstanceId, peerMac);
mPeerInfoByRequestorInstanceId.put(newPeerId, newPeerInfo);
- if (DBG) {
- Log.d(TAG, "New peer info: peerId=" + newPeerId + ", peerInfo=" + newPeerInfo);
+ if (VDBG) {
+ Log.v(TAG, "New peer info: peerId=" + newPeerId + ", peerInfo=" + newPeerInfo);
}
return newPeerId;
diff --git a/com/android/server/wifi/aware/WifiAwareMetrics.java b/com/android/server/wifi/aware/WifiAwareMetrics.java
index c45c6dca..4f5f46b0 100644
--- a/com/android/server/wifi/aware/WifiAwareMetrics.java
+++ b/com/android/server/wifi/aware/WifiAwareMetrics.java
@@ -40,7 +40,8 @@ import java.util.Set;
*/
public class WifiAwareMetrics {
private static final String TAG = "WifiAwareMetrics";
- private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+ /* package */ boolean mDbg = false;
// Histogram: 8 buckets (i=0, ..., 7) of 9 slots in range 10^i -> 10^(i+1)
// Buckets:
diff --git a/com/android/server/wifi/aware/WifiAwareNativeApi.java b/com/android/server/wifi/aware/WifiAwareNativeApi.java
index c8b5fced..66bbff52 100644
--- a/com/android/server/wifi/aware/WifiAwareNativeApi.java
+++ b/com/android/server/wifi/aware/WifiAwareNativeApi.java
@@ -56,8 +56,8 @@ import java.util.Map;
*/
public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellCommand {
private static final String TAG = "WifiAwareNativeApi";
- private static final boolean DBG = false;
private static final boolean VDBG = false; // STOPSHIP if true
+ /* package */ boolean mDbg = false;
private static final String SERVICE_NAME_FOR_OOB_DATA_PATH = "Wi-Fi Aware Data Path";
@@ -171,7 +171,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
* match with the original request.
*/
public boolean getCapabilities(short transactionId) {
- if (VDBG) Log.v(TAG, "getCapabilities: transactionId=" + transactionId);
+ if (mDbg) Log.v(TAG, "getCapabilities: transactionId=" + transactionId);
IWifiNanIface iface = mHal.getWifiNanIface();
if (iface == null) {
@@ -208,7 +208,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
public boolean enableAndConfigure(short transactionId, ConfigRequest configRequest,
boolean notifyIdentityChange, boolean initialConfiguration, boolean isInteractive,
boolean isIdle) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "enableAndConfigure: transactionId=" + transactionId + ", configRequest="
+ configRequest + ", notifyIdentityChange=" + notifyIdentityChange
+ ", initialConfiguration=" + initialConfiguration
@@ -370,7 +370,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
* used in the async callback to match with the original request.
*/
public boolean disable(short transactionId) {
- if (VDBG) Log.d(TAG, "disable");
+ if (mDbg) Log.d(TAG, "disable");
IWifiNanIface iface = mHal.getWifiNanIface();
if (iface == null) {
@@ -402,7 +402,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
* @param publishConfig Configuration of the discovery session.
*/
public boolean publish(short transactionId, byte publishId, PublishConfig publishConfig) {
- if (VDBG) {
+ if (mDbg) {
Log.d(TAG, "publish: transactionId=" + transactionId + ", publishId=" + publishId
+ ", config=" + publishConfig);
}
@@ -466,7 +466,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
*/
public boolean subscribe(short transactionId, byte subscribeId,
SubscribeConfig subscribeConfig) {
- if (VDBG) {
+ if (mDbg) {
Log.d(TAG, "subscribe: transactionId=" + transactionId + ", subscribeId=" + subscribeId
+ ", config=" + subscribeConfig);
}
@@ -546,7 +546,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
*/
public boolean sendMessage(short transactionId, byte pubSubId, int requestorInstanceId,
byte[] dest, byte[] message, int messageId) {
- if (VDBG) {
+ if (mDbg) {
Log.d(TAG,
"sendMessage: transactionId=" + transactionId + ", pubSubId=" + pubSubId
+ ", requestorInstanceId=" + requestorInstanceId + ", dest="
@@ -594,7 +594,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
* creating a session.
*/
public boolean stopPublish(short transactionId, byte pubSubId) {
- if (VDBG) {
+ if (mDbg) {
Log.d(TAG, "stopPublish: transactionId=" + transactionId + ", pubSubId=" + pubSubId);
}
@@ -627,7 +627,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
* creating a session.
*/
public boolean stopSubscribe(short transactionId, byte pubSubId) {
- if (VDBG) {
+ if (mDbg) {
Log.d(TAG, "stopSubscribe: transactionId=" + transactionId + ", pubSubId=" + pubSubId);
}
@@ -660,7 +660,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
* @param interfaceName The name of the interface, e.g. "aware0".
*/
public boolean createAwareNetworkInterface(short transactionId, String interfaceName) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "createAwareNetworkInterface: transactionId=" + transactionId + ", "
+ "interfaceName=" + interfaceName);
}
@@ -693,7 +693,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
* @param interfaceName The name of the interface, e.g. "aware0".
*/
public boolean deleteAwareNetworkInterface(short transactionId, String interfaceName) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "deleteAwareNetworkInterface: transactionId=" + transactionId + ", "
+ "interfaceName=" + interfaceName);
}
@@ -739,7 +739,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
public boolean initiateDataPath(short transactionId, int peerId, int channelRequestType,
int channel, byte[] peer, String interfaceName, byte[] pmk, String passphrase,
boolean isOutOfBand, Capabilities capabilities) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "initiateDataPath: transactionId=" + transactionId + ", peerId=" + peerId
+ ", channelRequestType=" + channelRequestType + ", channel=" + channel
+ ", peer=" + String.valueOf(HexEncoding.encode(peer)) + ", interfaceName="
@@ -816,7 +816,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
public boolean respondToDataPathRequest(short transactionId, boolean accept, int ndpId,
String interfaceName, byte[] pmk, String passphrase, boolean isOutOfBand,
Capabilities capabilities) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "respondToDataPathRequest: transactionId=" + transactionId + ", accept="
+ accept + ", int ndpId=" + ndpId + ", interfaceName=" + interfaceName);
}
@@ -878,7 +878,7 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
* @param ndpId The NDP (Aware data path) ID to be terminated.
*/
public boolean endDataPath(short transactionId, int ndpId) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "endDataPath: transactionId=" + transactionId + ", ndpId=" + ndpId);
}
diff --git a/com/android/server/wifi/aware/WifiAwareNativeCallback.java b/com/android/server/wifi/aware/WifiAwareNativeCallback.java
index b45978b2..78f8a28b 100644
--- a/com/android/server/wifi/aware/WifiAwareNativeCallback.java
+++ b/com/android/server/wifi/aware/WifiAwareNativeCallback.java
@@ -46,8 +46,8 @@ import java.util.Arrays;
public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub implements
WifiAwareShellCommand.DelegatedShellCommand {
private static final String TAG = "WifiAwareNativeCallback";
- private static final boolean DBG = false;
private static final boolean VDBG = false;
+ /* package */ boolean mDbg = false;
private final WifiAwareStateManager mWifiAwareStateManager;
@@ -141,7 +141,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyCapabilitiesResponse(short id, WifiNanStatus status,
NanCapabilities capabilities) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyCapabilitiesResponse: id=" + id + ", status=" + statusString(status)
+ ", capabilities=" + capabilities);
}
@@ -176,7 +176,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyEnableResponse(short id, WifiNanStatus status) {
- if (VDBG) Log.v(TAG, "notifyEnableResponse: id=" + id + ", status=" + statusString(status));
+ if (mDbg) Log.v(TAG, "notifyEnableResponse: id=" + id + ", status=" + statusString(status));
if (status.status == NanStatusType.ALREADY_ENABLED) {
Log.wtf(TAG, "notifyEnableResponse: id=" + id + ", already enabled!?");
@@ -192,7 +192,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyConfigResponse(short id, WifiNanStatus status) {
- if (VDBG) Log.v(TAG, "notifyConfigResponse: id=" + id + ", status=" + statusString(status));
+ if (mDbg) Log.v(TAG, "notifyConfigResponse: id=" + id + ", status=" + statusString(status));
if (status.status == NanStatusType.SUCCESS) {
mWifiAwareStateManager.onConfigSuccessResponse(id);
@@ -203,7 +203,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyDisableResponse(short id, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyDisableResponse: id=" + id + ", status=" + statusString(status));
}
@@ -216,7 +216,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyStartPublishResponse(short id, WifiNanStatus status, byte publishId) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyStartPublishResponse: id=" + id + ", status=" + statusString(status)
+ ", publishId=" + publishId);
}
@@ -230,7 +230,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyStopPublishResponse(short id, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyStopPublishResponse: id=" + id + ", status=" + statusString(status));
}
@@ -244,7 +244,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyStartSubscribeResponse(short id, WifiNanStatus status, byte subscribeId) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyStartSubscribeResponse: id=" + id + ", status=" + statusString(status)
+ ", subscribeId=" + subscribeId);
}
@@ -258,7 +258,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyStopSubscribeResponse(short id, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyStopSubscribeResponse: id=" + id + ", status="
+ statusString(status));
}
@@ -273,7 +273,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyTransmitFollowupResponse(short id, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyTransmitFollowupResponse: id=" + id + ", status="
+ statusString(status));
}
@@ -287,7 +287,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyCreateDataInterfaceResponse(short id, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyCreateDataInterfaceResponse: id=" + id + ", status="
+ statusString(status));
}
@@ -298,7 +298,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyDeleteDataInterfaceResponse(short id, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyDeleteDataInterfaceResponse: id=" + id + ", status="
+ statusString(status));
}
@@ -310,7 +310,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyInitiateDataPathResponse(short id, WifiNanStatus status,
int ndpInstanceId) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyInitiateDataPathResponse: id=" + id + ", status="
+ statusString(status) + ", ndpInstanceId=" + ndpInstanceId);
}
@@ -324,7 +324,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyRespondToDataPathIndicationResponse(short id, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyRespondToDataPathIndicationResponse: id=" + id
+ ", status=" + statusString(status));
}
@@ -335,7 +335,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void notifyTerminateDataPathResponse(short id, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "notifyTerminateDataPathResponse: id=" + id + ", status="
+ statusString(status));
}
@@ -346,7 +346,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventClusterEvent(NanClusterEventInd event) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "eventClusterEvent: eventType=" + event.eventType + ", addr="
+ String.valueOf(HexEncoding.encode(event.addr)));
}
@@ -367,7 +367,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventDisabled(WifiNanStatus status) {
- if (VDBG) Log.v(TAG, "eventDisabled: status=" + statusString(status));
+ if (mDbg) Log.v(TAG, "eventDisabled: status=" + statusString(status));
incrementCbCount(CB_EV_DISABLED);
mWifiAwareStateManager.onAwareDownNotification(status.status);
@@ -375,7 +375,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventPublishTerminated(byte sessionId, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "eventPublishTerminated: sessionId=" + sessionId + ", status="
+ statusString(status));
}
@@ -386,7 +386,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventSubscribeTerminated(byte sessionId, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "eventSubscribeTerminated: sessionId=" + sessionId + ", status="
+ statusString(status));
}
@@ -397,7 +397,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventMatch(NanMatchInd event) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "eventMatch: discoverySessionId=" + event.discoverySessionId + ", peerId="
+ event.peerId + ", addr=" + String.valueOf(HexEncoding.encode(event.addr))
+ ", serviceSpecificInfo=" + Arrays.toString(
@@ -420,7 +420,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventMatchExpired(byte discoverySessionId, int peerId) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "eventMatchExpired: discoverySessionId=" + discoverySessionId
+ ", peerId=" + peerId);
}
@@ -431,7 +431,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventFollowupReceived(NanFollowupReceivedInd event) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "eventFollowupReceived: discoverySessionId=" + event.discoverySessionId
+ ", peerId=" + event.peerId + ", addr=" + String.valueOf(
HexEncoding.encode(event.addr)) + ", serviceSpecificInfo=" + Arrays.toString(
@@ -446,7 +446,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventTransmitFollowup(short id, WifiNanStatus status) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "eventTransmitFollowup: id=" + id + ", status=" + statusString(status));
}
incrementCbCount(CB_EV_TRANSMIT_FOLLOWUP);
@@ -460,7 +460,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventDataPathRequest(NanDataPathRequestInd event) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "eventDataPathRequest: discoverySessionId=" + event.discoverySessionId
+ ", peerDiscMacAddr=" + String.valueOf(
HexEncoding.encode(event.peerDiscMacAddr)) + ", ndpInstanceId="
@@ -474,7 +474,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventDataPathConfirm(NanDataPathConfirmInd event) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "onDataPathConfirm: ndpInstanceId=" + event.ndpInstanceId
+ ", peerNdiMacAddr=" + String.valueOf(HexEncoding.encode(event.peerNdiMacAddr))
+ ", dataPathSetupSuccess=" + event.dataPathSetupSuccess + ", reason="
@@ -489,7 +489,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
@Override
public void eventDataPathTerminated(int ndpInstanceId) {
- if (VDBG) Log.v(TAG, "eventDataPathTerminated: ndpInstanceId=" + ndpInstanceId);
+ if (mDbg) Log.v(TAG, "eventDataPathTerminated: ndpInstanceId=" + ndpInstanceId);
incrementCbCount(CB_EV_DATA_PATH_TERMINATED);
mWifiAwareStateManager.onDataPathEndNotification(ndpInstanceId);
diff --git a/com/android/server/wifi/aware/WifiAwareNativeManager.java b/com/android/server/wifi/aware/WifiAwareNativeManager.java
index d6bec5f1..b9dd6808 100644
--- a/com/android/server/wifi/aware/WifiAwareNativeManager.java
+++ b/com/android/server/wifi/aware/WifiAwareNativeManager.java
@@ -36,7 +36,8 @@ import java.io.PrintWriter;
*/
public class WifiAwareNativeManager {
private static final String TAG = "WifiAwareNativeManager";
- private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+ /* package */ boolean mDbg = false;
// to be used for synchronizing access to any of the WifiAwareNative objects
private final Object mLock = new Object();
@@ -46,10 +47,10 @@ public class WifiAwareNativeManager {
private Handler mHandler;
private WifiAwareNativeCallback mWifiAwareNativeCallback;
private IWifiNanIface mWifiNanIface = null;
- private InterfaceDestroyedListener mInterfaceDestroyedListener =
- new InterfaceDestroyedListener();
+ private InterfaceDestroyedListener mInterfaceDestroyedListener;
private InterfaceAvailableForRequestListener mInterfaceAvailableForRequestListener =
new InterfaceAvailableForRequestListener();
+ private int mReferenceCount = 0;
WifiAwareNativeManager(WifiAwareStateManager awareStateManager,
HalDeviceManager halDeviceManager,
@@ -71,7 +72,7 @@ public class WifiAwareNativeManager {
new HalDeviceManager.ManagerStatusListener() {
@Override
public void onStatusChanged() {
- if (DBG) Log.d(TAG, "onStatusChanged");
+ if (VDBG) Log.v(TAG, "onStatusChanged");
// only care about isStarted (Wi-Fi started) not isReady - since if not
// ready then Wi-Fi will also be down.
if (mHalDeviceManager.isStarted()) {
@@ -83,11 +84,10 @@ public class WifiAwareNativeManager {
awareIsDown();
}
}
- }, null);
+ }, mHandler);
if (mHalDeviceManager.isStarted()) {
mHalDeviceManager.registerInterfaceAvailableForRequestListener(
IfaceType.NAN, mInterfaceAvailableForRequestListener, mHandler);
- tryToGetAware();
}
}
@@ -103,21 +103,33 @@ public class WifiAwareNativeManager {
}
/**
- * Attempt to obtain the HAL NAN interface. If available then enables Aware usage.
+ * Attempt to obtain the HAL NAN interface.
*/
- private void tryToGetAware() {
+ public void tryToGetAware() {
synchronized (mLock) {
- if (DBG) Log.d(TAG, "tryToGetAware: mWifiNanIface=" + mWifiNanIface);
+ if (mDbg) {
+ Log.d(TAG, "tryToGetAware: mWifiNanIface=" + mWifiNanIface + ", mReferenceCount="
+ + mReferenceCount);
+ }
if (mWifiNanIface != null) {
+ mReferenceCount++;
+ return;
+ }
+ if (mHalDeviceManager == null) {
+ Log.e(TAG, "tryToGetAware: mHalDeviceManager is null!?");
+ awareIsDown();
return;
}
+
+ mInterfaceDestroyedListener = new InterfaceDestroyedListener();
IWifiNanIface iface = mHalDeviceManager.createNanIface(mInterfaceDestroyedListener,
mHandler);
if (iface == null) {
- if (DBG) Log.d(TAG, "Was not able to obtain an IWifiNanIface");
+ Log.e(TAG, "Was not able to obtain an IWifiNanIface (even though enabled!?)");
+ awareIsDown();
} else {
- if (DBG) Log.d(TAG, "Obtained an IWifiNanIface");
+ if (mDbg) Log.v(TAG, "Obtained an IWifiNanIface");
try {
WifiStatus status = iface.registerEventCallback(mWifiAwareNativeCallback);
@@ -125,44 +137,92 @@ public class WifiAwareNativeManager {
Log.e(TAG, "IWifiNanIface.registerEventCallback error: " + statusString(
status));
mHalDeviceManager.removeIface(iface);
+ awareIsDown();
return;
}
} catch (RemoteException e) {
Log.e(TAG, "IWifiNanIface.registerEventCallback exception: " + e);
- mHalDeviceManager.removeIface(iface);
+ awareIsDown();
return;
}
mWifiNanIface = iface;
- mWifiAwareStateManager.enableUsage();
+ mReferenceCount = 1;
}
}
}
+ /**
+ * Release the HAL NAN interface.
+ */
+ public void releaseAware() {
+ if (mDbg) {
+ Log.d(TAG, "releaseAware: mWifiNanIface=" + mWifiNanIface + ", mReferenceCount="
+ + mReferenceCount);
+ }
+
+ if (mWifiNanIface == null) {
+ return;
+ }
+ if (mHalDeviceManager == null) {
+ Log.e(TAG, "releaseAware: mHalDeviceManager is null!?");
+ return;
+ }
+
+ synchronized (mLock) {
+ mReferenceCount--;
+ if (mReferenceCount != 0) {
+ return;
+ }
+ mInterfaceDestroyedListener.active = false;
+ mInterfaceDestroyedListener = null;
+ mHalDeviceManager.removeIface(mWifiNanIface);
+ mWifiNanIface = null;
+ }
+ }
+
private void awareIsDown() {
synchronized (mLock) {
- if (DBG) Log.d(TAG, "awareIsDown: mWifiNanIface=" + mWifiNanIface);
- if (mWifiNanIface != null) {
- mWifiNanIface = null;
- mWifiAwareStateManager.disableUsage();
+ if (mDbg) {
+ Log.d(TAG, "awareIsDown: mWifiNanIface=" + mWifiNanIface + ", mReferenceCount ="
+ + mReferenceCount);
}
+ mWifiNanIface = null;
+ mReferenceCount = 0;
+ mWifiAwareStateManager.disableUsage();
}
}
private class InterfaceDestroyedListener implements
HalDeviceManager.InterfaceDestroyedListener {
+ public boolean active = true;
+
@Override
public void onDestroyed(@NonNull String ifaceName) {
- if (DBG) Log.d(TAG, "Interface was destroyed");
- awareIsDown();
+ if (mDbg) {
+ Log.d(TAG, "Interface was destroyed: mWifiNanIface=" + mWifiNanIface + ", active="
+ + active);
+ }
+ if (active && mWifiNanIface != null) {
+ awareIsDown();
+ } // else: we released it locally so no need to disable usage
}
}
private class InterfaceAvailableForRequestListener implements
HalDeviceManager.InterfaceAvailableForRequestListener {
@Override
- public void onAvailableForRequest() {
- if (DBG) Log.d(TAG, "Interface is possibly available");
- tryToGetAware();
+ public void onAvailabilityChanged(boolean isAvailable) {
+ if (mDbg) {
+ Log.d(TAG, "Interface availability = " + isAvailable + ", mWifiNanIface="
+ + mWifiNanIface);
+ }
+ synchronized (mLock) {
+ if (isAvailable) {
+ mWifiAwareStateManager.enableUsage();
+ } else if (mWifiNanIface == null) { // not available could mean already have NAN
+ mWifiAwareStateManager.disableUsage();
+ }
+ }
}
}
@@ -181,6 +241,7 @@ public class WifiAwareNativeManager {
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("WifiAwareNativeManager:");
pw.println(" mWifiNanIface: " + mWifiNanIface);
+ pw.println(" mReferenceCount: " + mReferenceCount);
mWifiAwareNativeCallback.dump(fd, pw, args);
mHalDeviceManager.dump(fd, pw, args);
}
diff --git a/com/android/server/wifi/aware/WifiAwareService.java b/com/android/server/wifi/aware/WifiAwareService.java
index e210d3e1..f38c3731 100644
--- a/com/android/server/wifi/aware/WifiAwareService.java
+++ b/com/android/server/wifi/aware/WifiAwareService.java
@@ -69,7 +69,9 @@ public final class WifiAwareService extends SystemService {
HandlerThread awareHandlerThread = wifiInjector.getWifiAwareHandlerThread();
mImpl.start(awareHandlerThread, wifiAwareStateManager, wifiAwareShellCommand,
wifiInjector.getWifiMetrics().getWifiAwareMetrics(),
- wifiInjector.getWifiPermissionsWrapper());
+ wifiInjector.getWifiPermissionsUtil(),
+ wifiInjector.getWifiPermissionsWrapper(), wifiInjector.getFrameworkFacade(),
+ wifiAwareNativeManager, wifiAwareNativeApi, wifiAwareNativeCallback);
} else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
mImpl.startLate();
}
diff --git a/com/android/server/wifi/aware/WifiAwareServiceImpl.java b/com/android/server/wifi/aware/WifiAwareServiceImpl.java
index 421d9ac4..f92249e8 100644
--- a/com/android/server/wifi/aware/WifiAwareServiceImpl.java
+++ b/com/android/server/wifi/aware/WifiAwareServiceImpl.java
@@ -19,6 +19,7 @@ package com.android.server.wifi.aware;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.database.ContentObserver;
import android.hardware.wifi.V1_0.NanStatusType;
import android.net.wifi.aware.Characteristics;
import android.net.wifi.aware.ConfigRequest;
@@ -30,15 +31,20 @@ import android.net.wifi.aware.IWifiAwareManager;
import android.net.wifi.aware.PublishConfig;
import android.net.wifi.aware.SubscribeConfig;
import android.os.Binder;
+import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
+import android.provider.Settings;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import com.android.server.wifi.FrameworkFacade;
+import com.android.server.wifi.WifiInjector;
+import com.android.server.wifi.util.WifiPermissionsUtil;
import com.android.server.wifi.util.WifiPermissionsWrapper;
import java.io.FileDescriptor;
@@ -53,10 +59,11 @@ import java.util.List;
*/
public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
private static final String TAG = "WifiAwareService";
- private static final boolean DBG = false;
private static final boolean VDBG = false; // STOPSHIP if true
+ /* package */ boolean mDbg = false;
private Context mContext;
+ private WifiPermissionsUtil mWifiPermissionsUtil;
private WifiAwareStateManager mStateManager;
private WifiAwareShellCommand mShellCommand;
@@ -84,12 +91,56 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
*/
public void start(HandlerThread handlerThread, WifiAwareStateManager awareStateManager,
WifiAwareShellCommand awareShellCommand, WifiAwareMetrics awareMetrics,
- WifiPermissionsWrapper permissionsWrapper) {
+ WifiPermissionsUtil wifiPermissionsUtil, WifiPermissionsWrapper permissionsWrapper,
+ FrameworkFacade frameworkFacade, WifiAwareNativeManager wifiAwareNativeManager,
+ WifiAwareNativeApi wifiAwareNativeApi,
+ WifiAwareNativeCallback wifiAwareNativeCallback) {
Log.i(TAG, "Starting Wi-Fi Aware service");
+ mWifiPermissionsUtil = wifiPermissionsUtil;
mStateManager = awareStateManager;
mShellCommand = awareShellCommand;
mStateManager.start(mContext, handlerThread.getLooper(), awareMetrics, permissionsWrapper);
+
+ frameworkFacade.registerContentObserver(mContext,
+ Settings.Global.getUriFor(Settings.Global.WIFI_VERBOSE_LOGGING_ENABLED), true,
+ new ContentObserver(new Handler(handlerThread.getLooper())) {
+ @Override
+ public void onChange(boolean selfChange) {
+ enableVerboseLogging(frameworkFacade.getIntegerSetting(mContext,
+ Settings.Global.WIFI_VERBOSE_LOGGING_ENABLED, 0), awareStateManager,
+ wifiAwareNativeManager, wifiAwareNativeApi,
+ wifiAwareNativeCallback);
+ }
+ });
+ enableVerboseLogging(frameworkFacade.getIntegerSetting(mContext,
+ Settings.Global.WIFI_VERBOSE_LOGGING_ENABLED, 0), awareStateManager,
+ wifiAwareNativeManager, wifiAwareNativeApi, wifiAwareNativeCallback);
+ }
+
+ private void enableVerboseLogging(int verbose, WifiAwareStateManager awareStateManager,
+ WifiAwareNativeManager wifiAwareNativeManager, WifiAwareNativeApi wifiAwareNativeApi,
+ WifiAwareNativeCallback wifiAwareNativeCallback) {
+ boolean dbg;
+
+ if (verbose > 0) {
+ dbg = true;
+ } else {
+ dbg = false;
+ }
+ if (VDBG) {
+ dbg = true; // just override
+ }
+
+ mDbg = dbg;
+ awareStateManager.mDbg = dbg;
+ if (awareStateManager.mDataPathMgr != null) { // needed for unit tests
+ awareStateManager.mDataPathMgr.mDbg = dbg;
+ WifiInjector.getInstance().getWifiMetrics().getWifiAwareMetrics().mDbg = dbg;
+ }
+ wifiAwareNativeCallback.mDbg = dbg;
+ wifiAwareNativeManager.mDbg = dbg;
+ wifiAwareNativeApi.mDbg = dbg;
}
/**
@@ -130,7 +181,7 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
}
if (notifyOnIdentityChanged) {
- enforceLocationPermission();
+ enforceLocationPermission(callingPackage, getMockableCallingUid());
}
if (configRequest != null) {
@@ -148,7 +199,7 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
clientId = mNextClientId++;
}
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "connect: uid=" + uid + ", clientId=" + clientId + ", configRequest"
+ configRequest + ", notifyOnIdentityChanged=" + notifyOnIdentityChanged);
}
@@ -156,7 +207,7 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
IBinder.DeathRecipient dr = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
- if (DBG) Log.d(TAG, "binderDied: clientId=" + clientId);
+ if (mDbg) Log.v(TAG, "binderDied: clientId=" + clientId);
binder.unlinkToDeath(this, 0);
synchronized (mLock) {
@@ -196,7 +247,7 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
int uid = getMockableCallingUid();
enforceClientValidity(uid, clientId);
- if (VDBG) Log.v(TAG, "disconnect: uid=" + uid + ", clientId=" + clientId);
+ if (mDbg) Log.v(TAG, "disconnect: uid=" + uid + ", clientId=" + clientId);
if (binder == null) {
throw new IllegalArgumentException("Binder must not be null");
@@ -230,11 +281,11 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
}
@Override
- public void publish(int clientId, PublishConfig publishConfig,
+ public void publish(String callingPackage, int clientId, PublishConfig publishConfig,
IWifiAwareDiscoverySessionCallback callback) {
enforceAccessPermission();
enforceChangePermission();
- enforceLocationPermission();
+ enforceLocationPermission(callingPackage, getMockableCallingUid());
if (callback == null) {
throw new IllegalArgumentException("Callback must not be null");
@@ -242,7 +293,8 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
if (publishConfig == null) {
throw new IllegalArgumentException("PublishConfig must not be null");
}
- publishConfig.assertValid(mStateManager.getCharacteristics());
+ publishConfig.assertValid(mStateManager.getCharacteristics(),
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_RTT));
int uid = getMockableCallingUid();
enforceClientValidity(uid, clientId);
@@ -262,7 +314,8 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
if (publishConfig == null) {
throw new IllegalArgumentException("PublishConfig must not be null");
}
- publishConfig.assertValid(mStateManager.getCharacteristics());
+ publishConfig.assertValid(mStateManager.getCharacteristics(),
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_RTT));
int uid = getMockableCallingUid();
enforceClientValidity(uid, clientId);
@@ -275,11 +328,11 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
}
@Override
- public void subscribe(int clientId, SubscribeConfig subscribeConfig,
+ public void subscribe(String callingPackage, int clientId, SubscribeConfig subscribeConfig,
IWifiAwareDiscoverySessionCallback callback) {
enforceAccessPermission();
enforceChangePermission();
- enforceLocationPermission();
+ enforceLocationPermission(callingPackage, getMockableCallingUid());
if (callback == null) {
throw new IllegalArgumentException("Callback must not be null");
@@ -287,7 +340,8 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
if (subscribeConfig == null) {
throw new IllegalArgumentException("SubscribeConfig must not be null");
}
- subscribeConfig.assertValid(mStateManager.getCharacteristics());
+ subscribeConfig.assertValid(mStateManager.getCharacteristics(),
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_RTT));
int uid = getMockableCallingUid();
enforceClientValidity(uid, clientId);
@@ -307,7 +361,8 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
if (subscribeConfig == null) {
throw new IllegalArgumentException("SubscribeConfig must not be null");
}
- subscribeConfig.assertValid(mStateManager.getCharacteristics());
+ subscribeConfig.assertValid(mStateManager.getCharacteristics(),
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_RTT));
int uid = getMockableCallingUid();
enforceClientValidity(uid, clientId);
@@ -329,8 +384,8 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
enforceNetworkStackPermission();
}
- if (message != null
- && message.length > mStateManager.getCharacteristics().getMaxServiceNameLength()) {
+ if (message != null && message.length
+ > mStateManager.getCharacteristics().getMaxServiceSpecificInfoLength()) {
throw new IllegalArgumentException(
"Message length longer than supported by device characteristics");
}
@@ -399,9 +454,8 @@ public class WifiAwareServiceImpl extends IWifiAwareManager.Stub {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CHANGE_WIFI_STATE, TAG);
}
- private void enforceLocationPermission() {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION,
- TAG);
+ private void enforceLocationPermission(String callingPackage, int uid) {
+ mWifiPermissionsUtil.enforceLocationPermission(callingPackage, uid);
}
private void enforceNetworkStackPermission() {
diff --git a/com/android/server/wifi/aware/WifiAwareStateManager.java b/com/android/server/wifi/aware/WifiAwareStateManager.java
index 89d9a904..5ae69021 100644
--- a/com/android/server/wifi/aware/WifiAwareStateManager.java
+++ b/com/android/server/wifi/aware/WifiAwareStateManager.java
@@ -69,8 +69,8 @@ import java.util.Map;
*/
public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShellCommand {
private static final String TAG = "WifiAwareStateManager";
- private static final boolean DBG = false;
private static final boolean VDBG = false; // STOPSHIP if true
+ /* package */ boolean mDbg = false;
@VisibleForTesting
public static final String HAL_COMMAND_TIMEOUT_TAG = TAG + " HAL Command Timeout";
@@ -119,6 +119,8 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
private static final int COMMAND_TYPE_TRANSMIT_NEXT_MESSAGE = 119;
private static final int COMMAND_TYPE_RECONFIGURE = 120;
private static final int COMMAND_TYPE_DELAYED_INITIALIZATION = 121;
+ private static final int COMMAND_TYPE_GET_AWARE = 122;
+ private static final int COMMAND_TYPE_RELEASE_AWARE = 123;
private static final int RESPONSE_TYPE_ON_CONFIG_SUCCESS = 200;
private static final int RESPONSE_TYPE_ON_CONFIG_FAIL = 201;
@@ -344,7 +346,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
mContext = context;
mAwareMetrics = awareMetrics;
mSm = new WifiAwareStateMachine(TAG, looper);
- mSm.setDbg(DBG);
+ mSm.setDbg(VDBG);
mSm.start();
mDataPathMgr = new WifiAwareDataPathStateManager(this);
@@ -470,6 +472,26 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
}
/**
+ * Place a request to get the Wi-Fi Aware interface (before which no HAL command can be
+ * executed).
+ */
+ public void getAwareInterface() {
+ Message msg = mSm.obtainMessage(MESSAGE_TYPE_COMMAND);
+ msg.arg1 = COMMAND_TYPE_GET_AWARE;
+ mSm.sendMessage(msg);
+ }
+
+ /**
+ * Place a request to release the Wi-Fi Aware interface (after which no HAL command can be
+ * executed).
+ */
+ public void releaseAwareInterface() {
+ Message msg = mSm.obtainMessage(MESSAGE_TYPE_COMMAND);
+ msg.arg1 = COMMAND_TYPE_RELEASE_AWARE;
+ mSm.sendMessage(msg);
+ }
+
+ /**
* Place a request for a new client connection on the state machine queue.
*/
public void connect(int clientId, int uid, int pid, String callingPackage,
@@ -1121,7 +1143,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
WifiAwareNetworkSpecifier networkSpecifier =
(WifiAwareNetworkSpecifier) msg.obj;
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "MESSAGE_TYPE_DATA_PATH_TIMEOUT: networkSpecifier="
+ networkSpecifier);
}
@@ -1338,8 +1360,8 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
int retryCount = sentMessage.getData()
.getInt(MESSAGE_BUNDLE_KEY_RETRY_COUNT);
if (retryCount > 0 && reason == NanStatusType.NO_OTA_ACK) {
- if (DBG) {
- Log.d(TAG,
+ if (VDBG) {
+ Log.v(TAG,
"NOTIFICATION_TYPE_ON_MESSAGE_SEND_FAIL: transactionId="
+ transactionId + ", reason=" + reason
+ ": retransmitting - retryCount=" + retryCount);
@@ -1634,6 +1656,14 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
mWifiAwareNativeManager.start(getHandler());
waitForResponse = false;
break;
+ case COMMAND_TYPE_GET_AWARE:
+ mWifiAwareNativeManager.tryToGetAware();
+ waitForResponse = false;
+ break;
+ case COMMAND_TYPE_RELEASE_AWARE:
+ mWifiAwareNativeManager.releaseAware();
+ waitForResponse = false;
+ break;
default:
waitForResponse = false;
Log.wtf(TAG, "processCommand: this isn't a COMMAND -- msg=" + msg);
@@ -1775,7 +1805,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
}
private void processTimeout(Message msg) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "processTimeout: msg=" + msg);
}
@@ -1884,6 +1914,14 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
"processTimeout: COMMAND_TYPE_DELAYED_INITIALIZATION - shouldn't be "
+ "waiting!");
break;
+ case COMMAND_TYPE_GET_AWARE:
+ Log.wtf(TAG,
+ "processTimeout: COMMAND_TYPE_GET_AWARE - shouldn't be waiting!");
+ break;
+ case COMMAND_TYPE_RELEASE_AWARE:
+ Log.wtf(TAG,
+ "processTimeout: COMMAND_TYPE_RELEASE_AWARE - shouldn't be waiting!");
+ break;
default:
Log.wtf(TAG, "processTimeout: this isn't a COMMAND -- msg=" + msg);
/* fall-through */
@@ -1916,7 +1954,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
}
private void processSendMessageTimeout() {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "processSendMessageTimeout: mHostQueuedSendMessages.size()="
+ mHostQueuedSendMessages.size() + ", mFwQueuedSendMessages.size()="
+ mFwQueuedSendMessages.size() + ", mSendQueueBlocked="
@@ -1938,7 +1976,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
long messageEnqueueTime = message.getData().getLong(
MESSAGE_BUNDLE_KEY_SEND_MESSAGE_ENQUEUE_TIME);
if (first || messageEnqueueTime + AWARE_SEND_MESSAGE_TIMEOUT <= currentTime) {
- if (VDBG) {
+ if (mDbg) {
Log.v(TAG, "processSendMessageTimeout: expiring - transactionId="
+ transactionId + ", message=" + message
+ ", due to messageEnqueueTime=" + messageEnqueueTime
@@ -2051,6 +2089,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
WifiAwareClientState client = new WifiAwareClientState(mContext, clientId, uid, pid,
callingPackage, callback, configRequest, notifyIdentityChange,
SystemClock.elapsedRealtime());
+ client.mDbg = mDbg;
client.onInterfaceAddressChange(mCurrentDiscoveryInterfaceMac);
mClients.append(clientId, client);
mAwareMetrics.recordAttachSession(uid, notifyIdentityChange, mClients);
@@ -2059,6 +2098,10 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
boolean notificationRequired =
doesAnyClientNeedIdentityChangeNotifications() || notifyIdentityChange;
+ if (mCurrentAwareConfiguration == null) {
+ mWifiAwareNativeManager.tryToGetAware();
+ }
+
boolean success = mWifiAwareNativeApi.enableAndConfigure(transactionId, merged,
notificationRequired, mCurrentAwareConfiguration == null,
mPowerManager.isInteractive(), mPowerManager.isDeviceIdleMode());
@@ -2289,12 +2332,16 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
private void enableUsageLocal() {
if (VDBG) Log.v(TAG, "enableUsageLocal: mUsageEnabled=" + mUsageEnabled);
+ if (mCapabilities == null) {
+ getAwareInterface();
+ queryCapabilities();
+ releaseAwareInterface();
+ }
+
if (mUsageEnabled) {
return;
}
-
mUsageEnabled = true;
- queryCapabilities();
sendAwareStateChangedBroadcast(true);
mAwareMetrics.recordEnableUsage();
@@ -2399,6 +2446,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
WifiAwareClientState client = new WifiAwareClientState(mContext, clientId, uid, pid,
callingPackage, callback, configRequest, notifyIdentityChange,
SystemClock.elapsedRealtime());
+ client.mDbg = mDbg;
mClients.put(clientId, client);
mAwareMetrics.recordAttachSession(uid, notifyIdentityChange, mClients);
try {
@@ -2514,6 +2562,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
WifiAwareDiscoverySessionState session = new WifiAwareDiscoverySessionState(
mWifiAwareNativeApi, sessionId, pubSubId, callback, isPublish,
SystemClock.elapsedRealtime());
+ session.mDbg = mDbg;
client.addSession(session);
mAwareMetrics.recordDiscoverySession(client.getUid(),
@@ -2689,8 +2738,8 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
}
if (success) {
- if (DBG) {
- Log.d(TAG, "onCreateDataPathInterfaceResponseLocal: successfully created interface "
+ if (VDBG) {
+ Log.v(TAG, "onCreateDataPathInterfaceResponseLocal: successfully created interface "
+ command.obj);
}
mDataPathMgr.onInterfaceCreated((String) command.obj);
@@ -2710,8 +2759,8 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
}
if (success) {
- if (DBG) {
- Log.d(TAG, "onDeleteDataPathInterfaceResponseLocal: successfully deleted interface "
+ if (VDBG) {
+ Log.v(TAG, "onDeleteDataPathInterfaceResponseLocal: successfully deleted interface "
+ command.obj);
}
mDataPathMgr.onInterfaceDeleted((String) command.obj);
@@ -2860,7 +2909,10 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
private void onAwareDownLocal() {
if (VDBG) {
- Log.v(TAG, "onAwareDown");
+ Log.v(TAG, "onAwareDown: mCurrentAwareConfiguration=" + mCurrentAwareConfiguration);
+ }
+ if (mCurrentAwareConfiguration == null) {
+ return;
}
for (int i = 0; i < mClients.size(); ++i) {
diff --git a/com/android/server/wifi/hotspot2/OsuNetworkConnection.java b/com/android/server/wifi/hotspot2/OsuNetworkConnection.java
index f1bf1175..12305d4f 100644
--- a/com/android/server/wifi/hotspot2/OsuNetworkConnection.java
+++ b/com/android/server/wifi/hotspot2/OsuNetworkConnection.java
@@ -20,10 +20,12 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
import android.net.Network;
-import android.net.NetworkInfo;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
import android.os.Handler;
@@ -35,12 +37,17 @@ import android.util.Log;
*/
public class OsuNetworkConnection {
private static final String TAG = "OsuNetworkConnection";
+ private static final int TIMEOUT_MS = 10000;
private final Context mContext;
private boolean mVerboseLoggingEnabled = false;
private WifiManager mWifiManager;
+ private ConnectivityManager mConnectivityManager;
+ private ConnectivityCallbacks mConnectivityCallbacks;
private Callbacks mCallbacks;
+ private Handler mHandler;
+ private Network mNetwork = null;
private boolean mConnected = false;
private int mNetworkId = -1;
private boolean mWifiEnabled = false;
@@ -91,26 +98,32 @@ public class OsuNetworkConnection {
*/
public void init(Handler handler) {
IntentFilter filter = new IntentFilter();
- filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
- handleNetworkStateChanged(
- intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO),
- intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO));
- } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
+ if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
WifiManager.WIFI_STATE_UNKNOWN);
- handleWifiStateChanged(state);
+ if (state == WifiManager.WIFI_STATE_DISABLED && mWifiEnabled) {
+ mWifiEnabled = false;
+ if (mCallbacks != null) mCallbacks.onWifiDisabled();
+ }
+ if (state == WifiManager.WIFI_STATE_ENABLED && !mWifiEnabled) {
+ mWifiEnabled = true;
+ if (mCallbacks != null) mCallbacks.onWifiEnabled();
+ }
}
}
};
mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
mContext.registerReceiver(receiver, filter, null, handler);
mWifiEnabled = mWifiManager.isWifiEnabled();
+ mConnectivityManager =
+ (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mConnectivityCallbacks = new ConnectivityCallbacks();
+ mHandler = handler;
}
/**
@@ -127,10 +140,8 @@ public class OsuNetworkConnection {
}
mWifiManager.removeNetwork(mNetworkId);
mNetworkId = -1;
+ mNetwork = null;
mConnected = false;
- if (mCallbacks != null) {
- mCallbacks.onDisconnected();
- }
}
/**
@@ -159,7 +170,7 @@ public class OsuNetworkConnection {
}
return true;
}
- if (!mWifiManager.isWifiEnabled()) {
+ if (!mWifiEnabled) {
Log.w(TAG, "Wifi is not enabled");
return false;
}
@@ -177,6 +188,11 @@ public class OsuNetworkConnection {
Log.e(TAG, "Unable to add network");
return false;
}
+ NetworkRequest networkRequest = null;
+ networkRequest = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
+ mConnectivityManager.requestNetwork(networkRequest, mConnectivityCallbacks, mHandler,
+ TIMEOUT_MS);
if (!mWifiManager.enableNetwork(mNetworkId, true)) {
Log.e(TAG, "Unable to enable network " + mNetworkId);
disconnectIfNeeded();
@@ -185,7 +201,6 @@ public class OsuNetworkConnection {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Current network ID " + mNetworkId);
}
- // TODO(sohanirao): setup alarm to time out the connection attempt.
return true;
}
@@ -197,74 +212,45 @@ public class OsuNetworkConnection {
mVerboseLoggingEnabled = verbose > 0 ? true : false;
}
- /**
- * Handle network state changed events.
- *
- * @param networkInfo {@link NetworkInfo} indicating the current network state
- * @param wifiInfo {@link WifiInfo} associated with the current network when connected
- */
- private void handleNetworkStateChanged(NetworkInfo networkInfo, WifiInfo wifiInfo) {
- if (networkInfo == null) {
- Log.w(TAG, "NetworkInfo not provided for network state changed event");
- return;
- }
- switch (networkInfo.getDetailedState()) {
- case CONNECTED:
- if (mVerboseLoggingEnabled) {
- Log.v(TAG, "Connected event received");
- }
- if (wifiInfo == null) {
- Log.w(TAG, "WifiInfo not provided for network state changed event");
- return;
- }
- handleConnectedEvent(wifiInfo);
- break;
- case DISCONNECTED:
- if (mVerboseLoggingEnabled) {
- Log.v(TAG, "Disconnected event received");
- }
- disconnectIfNeeded();
- break;
- default:
- if (mVerboseLoggingEnabled) {
- Log.v(TAG, "Ignore uninterested state: " + networkInfo.getDetailedState());
+ private class ConnectivityCallbacks extends ConnectivityManager.NetworkCallback {
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "onLinkPropertiesChanged for network=" + network
+ + " isProvisioned?" + linkProperties.isProvisioned());
+ }
+ if (linkProperties.isProvisioned() && mNetwork == null) {
+ mNetwork = network;
+ mConnected = true;
+ if (mCallbacks != null) {
+ mCallbacks.onConnected(network);
}
- break;
+ }
}
- }
- /**
- * Handle network connected event.
- *
- * @param wifiInfo {@link WifiInfo} associated with the current connection
- */
- private void handleConnectedEvent(WifiInfo wifiInfo) {
- if (mVerboseLoggingEnabled) {
- Log.v(TAG, "handleConnectedEvent " + wifiInfo.getNetworkId());
- }
- if (wifiInfo.getNetworkId() != mNetworkId) {
- disconnectIfNeeded();
- return;
- }
- if (!mConnected) {
- mConnected = true;
+ @Override
+ public void onUnavailable() {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "onUnvailable ");
+ }
if (mCallbacks != null) {
- mCallbacks.onConnected(mWifiManager.getCurrentNetwork());
+ mCallbacks.onTimeOut();
}
}
- }
- /**
- * Handle Wifi state change event
- */
- private void handleWifiStateChanged(int state) {
- if (state == WifiManager.WIFI_STATE_DISABLED && mWifiEnabled) {
- mWifiEnabled = false;
- if (mCallbacks != null) mCallbacks.onWifiDisabled();
- }
- if (state == WifiManager.WIFI_STATE_ENABLED && !mWifiEnabled) {
- mWifiEnabled = true;
- if (mCallbacks != null) mCallbacks.onWifiEnabled();
+ @Override
+ public void onLost(Network network) {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "onLost " + network);
+ }
+ if (network != mNetwork) {
+ Log.w(TAG, "Irrelevant network lost notification");
+ return;
+ }
+ if (mCallbacks != null) {
+ mCallbacks.onDisconnected();
+ }
}
}
}
+
diff --git a/com/android/server/wifi/hotspot2/OsuServerConnection.java b/com/android/server/wifi/hotspot2/OsuServerConnection.java
new file mode 100644
index 00000000..469100cb
--- /dev/null
+++ b/com/android/server/wifi/hotspot2/OsuServerConnection.java
@@ -0,0 +1,191 @@
+/*
+ * 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 com.android.server.wifi.hotspot2;
+
+import android.net.Network;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Provides methods to interface with the OSU server
+ */
+public class OsuServerConnection {
+ private static final String TAG = "OsuServerConnection";
+
+ private static final String HTTP = "http";
+ private static final String HTTPS = "https";
+
+ private SSLSocketFactory mSocketFactory;
+ private URL mUrl;
+ private Network mNetwork;
+ private WFATrustManager mTrustManager;
+ private HttpsURLConnection mUrlConnection = null;
+ private PasspointProvisioner.OsuServerCallbacks mOsuServerCallbacks;
+ private boolean mSetupComplete = false;
+ private boolean mVerboseLoggingEnabled = false;
+
+ /**
+ * Sets up callback for event
+ * @param callbacks OsuServerCallbacks to be invoked for server related events
+ */
+ public void setEventCallback(PasspointProvisioner.OsuServerCallbacks callbacks) {
+ mOsuServerCallbacks = callbacks;
+ }
+
+ /**
+ * Initialize socket factory for server connection using HTTPS
+ * @param tlsVersion String indicating the TLS version that will be used
+ */
+ public void init(String tlsVersion) {
+ // TODO(sohanirao) : Create and pass in a custom WFA Keystore
+ try {
+ // TODO(sohanirao) : Use the custom key store instead
+ mTrustManager = new WFATrustManager(KeyStore.getInstance("AndroidCAStore"));
+ SSLContext tlsContext = SSLContext.getInstance(tlsVersion);
+ tlsContext.init(null, new TrustManager[] { mTrustManager }, null);
+ mSocketFactory = tlsContext.getSocketFactory();
+ } catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
+ Log.w(TAG, "Initialization failed");
+ e.printStackTrace();
+ return;
+ }
+ mSetupComplete = true;
+ }
+
+ /**
+ * Provides the capability to run OSU server validation
+ * @return boolean true if capability available
+ */
+ public boolean canValidateServer() {
+ return mSetupComplete;
+ }
+
+ /**
+ * Enables verbose logging
+ * @param verbose a value greater than zero enables verbose logging
+ */
+ public void enableVerboseLogging(int verbose) {
+ mVerboseLoggingEnabled = verbose > 0 ? true : false;
+ }
+
+ /**
+ * Connect to the OSU server
+ * @param url Osu Server's URL
+ * @param network current network connection
+ * @return boolean value, true if connection was successful
+ *
+ * Relies on the caller to ensure that the capability to validate the OSU
+ * Server is available.
+ */
+ public boolean connect(URL url, Network network) {
+ mNetwork = network;
+ mUrl = url;
+ HttpsURLConnection urlConnection = null;
+ try {
+ urlConnection = (HttpsURLConnection) mNetwork.openConnection(mUrl);
+ urlConnection.setSSLSocketFactory(mSocketFactory);
+ urlConnection.connect();
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to establish a URL connection");
+ e.printStackTrace();
+ return false;
+ }
+ mUrlConnection = urlConnection;
+ return true;
+ }
+
+ /**
+ * Validate the OSU server
+ */
+ public boolean validateProvider(String friendlyName) {
+ X509Certificate[] certs = mTrustManager.getTrustChain();
+ if (certs == null) {
+ return false;
+ }
+ // TODO(sohanirao) : Validate friendly name
+ return true;
+ }
+
+ /**
+ * Clean up
+ */
+ public void cleanup() {
+ mUrlConnection.disconnect();
+ }
+
+ private class WFATrustManager implements X509TrustManager {
+ private X509Certificate[] mTrustChain;
+ KeyStore mKeyStore;
+
+ WFATrustManager(KeyStore ks) {
+ mKeyStore = ks;
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "checkClientTrusted " + authType);
+ }
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "checkServerTrusted " + authType);
+ }
+ boolean certsValid = false;
+ for (X509Certificate cert : chain) {
+ // TODO(sohanirao) : Validation of certs
+ certsValid = true;
+ }
+ mTrustChain = chain;
+ if (mOsuServerCallbacks != null) {
+ mOsuServerCallbacks.onServerValidationStatus(
+ mOsuServerCallbacks.getSessionId(), certsValid);
+ }
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "getAcceptedIssuers ");
+ }
+ return null;
+ }
+
+ public X509Certificate[] getTrustChain() {
+ return mTrustChain;
+ }
+ }
+}
+
diff --git a/com/android/server/wifi/hotspot2/PasspointConfigStoreData.java b/com/android/server/wifi/hotspot2/PasspointConfigStoreData.java
index 38401d2c..553a38bf 100644
--- a/com/android/server/wifi/hotspot2/PasspointConfigStoreData.java
+++ b/com/android/server/wifi/hotspot2/PasspointConfigStoreData.java
@@ -307,6 +307,7 @@ public class PasspointConfigStoreData implements WifiConfigStore.StoreData {
String clientCertificateAlias = null;
String clientPrivateKeyAlias = null;
boolean hasEverConnected = false;
+ boolean shared = false;
PasspointConfiguration config = null;
while (XmlUtils.nextElementWithin(in, outerTagDepth)) {
if (in.getAttributeValue(null, "name") != null) {
@@ -351,7 +352,7 @@ public class PasspointConfigStoreData implements WifiConfigStore.StoreData {
}
return new PasspointProvider(config, mKeyStore, mSimAccessor, providerId, creatorUid,
caCertificateAlias, clientCertificateAlias, clientPrivateKeyAlias,
- hasEverConnected);
+ hasEverConnected, shared);
}
/**
diff --git a/com/android/server/wifi/hotspot2/PasspointManager.java b/com/android/server/wifi/hotspot2/PasspointManager.java
index fec3dd81..329306a6 100644
--- a/com/android/server/wifi/hotspot2/PasspointManager.java
+++ b/com/android/server/wifi/hotspot2/PasspointManager.java
@@ -216,7 +216,8 @@ public class PasspointManager {
wifiConfigStore.registerStoreData(objectFactory.makePasspointConfigStoreData(
mKeyStore, mSimAccessor, new DataSourceHandler()));
mPasspointProvisioner = objectFactory.makePasspointProvisioner(context,
- objectFactory.makeOsuNetworkConnection(context));
+ objectFactory.makeOsuNetworkConnection(context),
+ objectFactory.makeOsuServerConnection());
sPasspointManager = this;
}
@@ -720,7 +721,7 @@ public class PasspointManager {
mSimAccessor, mProviderIndex++, wifiConfig.creatorUid,
enterpriseConfig.getCaCertificateAlias(),
enterpriseConfig.getClientCertificateAlias(),
- enterpriseConfig.getClientCertificateAlias(), false);
+ enterpriseConfig.getClientCertificateAlias(), false, false);
mProviders.put(passpointConfig.getHomeSp().getFqdn(), provider);
return true;
}
diff --git a/com/android/server/wifi/hotspot2/PasspointObjectFactory.java b/com/android/server/wifi/hotspot2/PasspointObjectFactory.java
index 71fc47a2..6cf3a7ed 100644
--- a/com/android/server/wifi/hotspot2/PasspointObjectFactory.java
+++ b/com/android/server/wifi/hotspot2/PasspointObjectFactory.java
@@ -104,8 +104,10 @@ public class PasspointObjectFactory{
* @return {@link PasspointProvisioner}
*/
public PasspointProvisioner makePasspointProvisioner(Context context,
- OsuNetworkConnection osuNetworkConnection) {
- return new PasspointProvisioner(context, osuNetworkConnection);
+ OsuNetworkConnection osuNetworkConnection,
+ OsuServerConnection osuServerConnection) {
+ return new PasspointProvisioner(context, osuNetworkConnection,
+ osuServerConnection);
}
/**
@@ -117,4 +119,13 @@ public class PasspointObjectFactory{
public OsuNetworkConnection makeOsuNetworkConnection(Context context) {
return new OsuNetworkConnection(context);
}
+
+ /**
+ * Create an instance of {@link OsuServerConnection}.
+ *
+ * @return {@link OsuServerConnection}
+ */
+ public OsuServerConnection makeOsuServerConnection() {
+ return new OsuServerConnection();
+ }
}
diff --git a/com/android/server/wifi/hotspot2/PasspointProvider.java b/com/android/server/wifi/hotspot2/PasspointProvider.java
index c7943ed8..23ab57ff 100644
--- a/com/android/server/wifi/hotspot2/PasspointProvider.java
+++ b/com/android/server/wifi/hotspot2/PasspointProvider.java
@@ -88,16 +88,17 @@ public class PasspointProvider {
private final AuthParam mAuthParam;
private boolean mHasEverConnected;
+ private boolean mIsShared;
public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore,
SIMAccessor simAccessor, long providerId, int creatorUid) {
- this(config, keyStore, simAccessor, providerId, creatorUid, null, null, null, false);
+ this(config, keyStore, simAccessor, providerId, creatorUid, null, null, null, false, false);
}
public PasspointProvider(PasspointConfiguration config, WifiKeyStore keyStore,
SIMAccessor simAccessor, long providerId, int creatorUid, String caCertificateAlias,
String clientCertificateAlias, String clientPrivateKeyAlias,
- boolean hasEverConnected) {
+ boolean hasEverConnected, boolean isShared) {
// Maintain a copy of the configuration to avoid it being updated by others.
mConfig = new PasspointConfiguration(config);
mKeyStore = keyStore;
@@ -107,6 +108,7 @@ public class PasspointProvider {
mClientCertificateAlias = clientCertificateAlias;
mClientPrivateKeyAlias = clientPrivateKeyAlias;
mHasEverConnected = hasEverConnected;
+ mIsShared = isShared;
// Setup EAP method and authentication parameter based on the credential.
if (mConfig.getCredential().getUserCredential() != null) {
@@ -311,6 +313,7 @@ public class PasspointProvider {
mConfig.getCredential().getSimCredential());
}
wifiConfig.enterpriseConfig = enterpriseConfig;
+ wifiConfig.shared = mIsShared;
return wifiConfig;
}
diff --git a/com/android/server/wifi/hotspot2/PasspointProvisioner.java b/com/android/server/wifi/hotspot2/PasspointProvisioner.java
index d3bb7731..c45a2843 100644
--- a/com/android/server/wifi/hotspot2/PasspointProvisioner.java
+++ b/com/android/server/wifi/hotspot2/PasspointProvisioner.java
@@ -26,28 +26,38 @@ import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
+import java.net.MalformedURLException;
+import java.net.URL;
/**
* Provides methods to carry out provisioning flow
*/
public class PasspointProvisioner {
private static final String TAG = "PasspointProvisioner";
+ // Indicates callback type for caller initiating provisioning
private static final int PROVISIONING_STATUS = 0;
private static final int PROVISIONING_FAILURE = 1;
+ // TLS version to be used for HTTPS connection with OSU server
+ private static final String TLS_VERSION = "TLSv1";
+
private final Context mContext;
private final ProvisioningStateMachine mProvisioningStateMachine;
private final OsuNetworkCallbacks mOsuNetworkCallbacks;
private final OsuNetworkConnection mOsuNetworkConnection;
+ private final OsuServerConnection mOsuServerConnection;
+ private int mCurrentSessionId = 0;
private int mCallingUid;
private boolean mVerboseLoggingEnabled = false;
- PasspointProvisioner(Context context, OsuNetworkConnection osuNetworkConnection) {
+ PasspointProvisioner(Context context, OsuNetworkConnection osuNetworkConnection,
+ OsuServerConnection osuServerConnection) {
mContext = context;
mOsuNetworkConnection = osuNetworkConnection;
mProvisioningStateMachine = new ProvisioningStateMachine();
mOsuNetworkCallbacks = new OsuNetworkCallbacks();
+ mOsuServerConnection = osuServerConnection;
}
/**
@@ -57,6 +67,7 @@ public class PasspointProvisioner {
public void init(Looper looper) {
mProvisioningStateMachine.start(new Handler(looper));
mOsuNetworkConnection.init(mProvisioningStateMachine.getHandler());
+ mOsuServerConnection.init(TLS_VERSION);
}
/**
@@ -66,6 +77,7 @@ public class PasspointProvisioner {
public void enableVerboseLogging(int level) {
mVerboseLoggingEnabled = (level > 0) ? true : false;
mOsuNetworkConnection.enableVerboseLogging(level);
+ mOsuServerConnection.enableVerboseLogging(level);
}
/**
@@ -96,27 +108,24 @@ public class PasspointProvisioner {
class ProvisioningStateMachine {
private static final String TAG = "ProvisioningStateMachine";
- private static final int DEFAULT_STATE = 0;
private static final int INITIAL_STATE = 1;
private static final int WAITING_TO_CONNECT = 2;
private static final int OSU_AP_CONNECTED = 3;
- private static final int FAILED_STATE = 4;
+ private static final int OSU_SERVER_CONNECTED = 4;
+ private static final int OSU_SERVER_VALIDATED = 5;
+ private static final int OSU_PROVIDER_VERIFIED = 6;
private OsuProvider mOsuProvider;
private IProvisioningCallback mProvisioningCallback;
+ private int mState = INITIAL_STATE;
private Handler mHandler;
- private int mState;
-
- ProvisioningStateMachine() {
- mState = DEFAULT_STATE;
- }
+ private URL mServerUrl;
/**
* Initializes and starts the state machine with a handler to handle incoming events
*/
public void start(Handler handler) {
mHandler = handler;
- changeState(INITIAL_STATE);
}
/**
@@ -137,25 +146,43 @@ public class PasspointProvisioner {
Log.v(TAG, "startProvisioning received in state=" + mState);
}
if (mState != INITIAL_STATE) {
- Log.v(TAG, "State Machine needs to be reset before starting provisioning");
- resetStateMachine();
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "State Machine needs to be reset before starting provisioning");
+ }
+ resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
+ }
+ if (!mOsuServerConnection.canValidateServer()) {
+ Log.w(TAG, "Provisioning is not possible");
+ mProvisioningCallback = callback;
+ resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_NOT_AVAILABLE);
+ return;
}
+ URL serverUrl = null;
+ try {
+ serverUrl = new URL(provider.getServerUri().toString());
+ } catch (MalformedURLException e) {
+ Log.e(TAG, "Invalid Server URL");
+ mProvisioningCallback = callback;
+ resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_URL_INVALID);
+ return;
+ }
+ mServerUrl = serverUrl;
mProvisioningCallback = callback;
mOsuProvider = provider;
-
// Register for network and wifi state events during provisioning flow
mOsuNetworkConnection.setEventCallback(mOsuNetworkCallbacks);
- if (mOsuNetworkConnection.connect(mOsuProvider.getOsuSsid(),
+ // Register for OSU server callbacks
+ mOsuServerConnection.setEventCallback(new OsuServerCallbacks(++mCurrentSessionId));
+
+ if (!mOsuNetworkConnection.connect(mOsuProvider.getOsuSsid(),
mOsuProvider.getNetworkAccessIdentifier())) {
- invokeProvisioningCallback(PROVISIONING_STATUS,
- ProvisioningCallback.OSU_STATUS_AP_CONNECTING);
- changeState(WAITING_TO_CONNECT);
- } else {
- invokeProvisioningCallback(PROVISIONING_FAILURE,
- ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
- enterFailedState();
+ resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
+ return;
}
+ invokeProvisioningCallback(PROVISIONING_STATUS,
+ ProvisioningCallback.OSU_STATUS_AP_CONNECTING);
+ changeState(WAITING_TO_CONNECT);
}
/**
@@ -165,23 +192,66 @@ public class PasspointProvisioner {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Wifi Disabled in state=" + mState);
}
- if (mState == INITIAL_STATE || mState == FAILED_STATE) {
+ if (mState == INITIAL_STATE) {
Log.w(TAG, "Wifi Disable unhandled in state=" + mState);
return;
}
- invokeProvisioningCallback(PROVISIONING_FAILURE,
- ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
- enterFailedState();
+ resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
}
- private void resetStateMachine() {
- // Set to null so that no callbacks are invoked during reset
- mProvisioningCallback = null;
- if (mState != INITIAL_STATE || mState != FAILED_STATE) {
- // Transition through Failed state to clean up
- enterFailedState();
+ /**
+ * Handle server validation failure
+ */
+ public void handleServerValidationFailure(int sessionId) {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Server Validation failure received in " + mState);
}
- changeState(INITIAL_STATE);
+ if (sessionId != mCurrentSessionId) {
+ Log.w(TAG, "Expected server validation callback for currentSessionId="
+ + mCurrentSessionId);
+ return;
+ }
+ if (mState != OSU_SERVER_CONNECTED) {
+ Log.wtf(TAG, "Server Validation Failure unhandled in mState=" + mState);
+ return;
+ }
+ resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_VALIDATION);
+ }
+
+ /**
+ * Handle status of server validation success
+ */
+ public void handleServerValidationSuccess(int sessionId) {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Server Validation Success received in " + mState);
+ }
+ if (sessionId != mCurrentSessionId) {
+ Log.w(TAG, "Expected server validation callback for currentSessionId="
+ + mCurrentSessionId);
+ return;
+ }
+ if (mState != OSU_SERVER_CONNECTED) {
+ Log.wtf(TAG, "Server validation success event unhandled in state=" + mState);
+ return;
+ }
+ changeState(OSU_SERVER_VALIDATED);
+ invokeProvisioningCallback(PROVISIONING_STATUS,
+ ProvisioningCallback.OSU_STATUS_SERVER_VALIDATED);
+ validateProvider();
+ }
+
+ private void validateProvider() {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Validating provider in state=" + mState);
+ }
+ if (!mOsuServerConnection.validateProvider(mOsuProvider.getFriendlyName())) {
+ resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVIDER_VERIFICATION);
+ return;
+ }
+ changeState(OSU_PROVIDER_VERIFIED);
+ invokeProvisioningCallback(PROVISIONING_STATUS,
+ ProvisioningCallback.OSU_STATUS_PROVIDER_VERIFIED);
+ // TODO(sohanirao) : send Initial SOAP Exchange
}
/**
@@ -194,12 +264,30 @@ public class PasspointProvisioner {
}
if (mState != WAITING_TO_CONNECT) {
// Not waiting for a connection
- Log.w(TAG, "Connection event unhandled in state=" + mState);
+ Log.wtf(TAG, "Connection event unhandled in state=" + mState);
return;
}
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_AP_CONNECTED);
changeState(OSU_AP_CONNECTED);
+ initiateServerConnection(network);
+ }
+
+ private void initiateServerConnection(Network network) {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Initiating server connection in state=" + mState);
+ }
+ if (mState != OSU_AP_CONNECTED) {
+ Log.wtf(TAG , "Initiating server connection aborted in invalid state=" + mState);
+ return;
+ }
+ if (!mOsuServerConnection.connect(mServerUrl, network)) {
+ resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION);
+ return;
+ }
+ changeState(OSU_SERVER_CONNECTED);
+ invokeProvisioningCallback(PROVISIONING_STATUS,
+ ProvisioningCallback.OSU_STATUS_SERVER_CONNECTED);
}
/**
@@ -209,28 +297,17 @@ public class PasspointProvisioner {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Connection failed in state=" + mState);
}
- if (mState == INITIAL_STATE || mState == FAILED_STATE) {
+ if (mState == INITIAL_STATE) {
Log.w(TAG, "Disconnect event unhandled in state=" + mState);
return;
}
- invokeProvisioningCallback(PROVISIONING_FAILURE,
- ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
- enterFailedState();
- }
-
- private void changeState(int nextState) {
- if (nextState != mState) {
- if (mVerboseLoggingEnabled) {
- Log.v(TAG, "Changing state from " + mState + " -> " + nextState);
- }
- mState = nextState;
- }
+ resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
}
private void invokeProvisioningCallback(int callbackType, int status) {
if (mProvisioningCallback == null) {
Log.e(TAG, "Provisioning callback " + callbackType + " with status " + status
- + " not invoked, callback is null");
+ + " not invoked");
return;
}
try {
@@ -240,18 +317,27 @@ public class PasspointProvisioner {
mProvisioningCallback.onProvisioningFailure(status);
}
} catch (RemoteException e) {
- if (callbackType == PROVISIONING_STATUS) {
- Log.e(TAG, "Remote Exception while posting Provisioning status " + status);
- } else {
- Log.e(TAG, "Remote Exception while posting Provisioning failure " + status);
+ Log.e(TAG, "Remote Exception while posting callback type=" + callbackType
+ + " status=" + status);
+ }
+ }
+
+ private void changeState(int nextState) {
+ if (nextState != mState) {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Changing state from " + mState + " -> " + nextState);
}
+ mState = nextState;
}
}
- private void enterFailedState() {
- changeState(FAILED_STATE);
+ private void resetStateMachine(int failureCode) {
+ invokeProvisioningCallback(PROVISIONING_FAILURE, failureCode);
mOsuNetworkConnection.setEventCallback(null);
mOsuNetworkConnection.disconnectIfNeeded();
+ mOsuServerConnection.setEventCallback(null);
+ mOsuServerConnection.cleanup();
+ changeState(INITIAL_STATE);
}
}
@@ -260,50 +346,90 @@ public class PasspointProvisioner {
*/
class OsuNetworkCallbacks implements OsuNetworkConnection.Callbacks {
- OsuNetworkCallbacks() {
- }
+ OsuNetworkCallbacks() {}
@Override
public void onConnected(Network network) {
- Log.v(TAG, "onConnected to " + network);
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "onConnected to " + network);
+ }
if (network == null) {
- mProvisioningStateMachine.getHandler().post(() -> {
- mProvisioningStateMachine.handleDisconnect();
- });
+ mProvisioningStateMachine.handleDisconnect();
} else {
- mProvisioningStateMachine.getHandler().post(() -> {
- mProvisioningStateMachine.handleConnectedEvent(network);
- });
+ mProvisioningStateMachine.handleConnectedEvent(network);
}
}
@Override
public void onDisconnected() {
- Log.v(TAG, "onDisconnected");
- mProvisioningStateMachine.getHandler().post(() -> {
- mProvisioningStateMachine.handleDisconnect();
- });
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "onDisconnected");
+ }
+ mProvisioningStateMachine.handleDisconnect();
}
@Override
public void onTimeOut() {
- Log.v(TAG, "Timed out waiting for connection to OSU AP");
- mProvisioningStateMachine.getHandler().post(() -> {
- mProvisioningStateMachine.handleDisconnect();
- });
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "Timed out waiting for connection to OSU AP");
+ }
+ mProvisioningStateMachine.handleDisconnect();
}
@Override
public void onWifiEnabled() {
- Log.v(TAG, "onWifiEnabled");
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "onWifiEnabled");
+ }
}
@Override
public void onWifiDisabled() {
- Log.v(TAG, "onWifiDisabled");
- mProvisioningStateMachine.getHandler().post(() -> {
- mProvisioningStateMachine.handleWifiDisabled();
- });
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "onWifiDisabled");
+ }
+ mProvisioningStateMachine.handleWifiDisabled();
+ }
+ }
+
+ /**
+ * Defines the callbacks expected from OsuServerConnection
+ */
+ public class OsuServerCallbacks {
+ private final int mSessionId;
+
+ OsuServerCallbacks(int sessionId) {
+ mSessionId = sessionId;
+ }
+
+ /**
+ * Returns the session ID corresponding to this callback
+ * @return int sessionID
+ */
+ public int getSessionId() {
+ return mSessionId;
}
+
+ /**
+ * Provides a server validation status for the session ID
+ * @param sessionId integer indicating current session ID
+ * @param succeeded boolean indicating success/failure of server validation
+ */
+ public void onServerValidationStatus(int sessionId, boolean succeeded) {
+ if (mVerboseLoggingEnabled) {
+ Log.v(TAG, "OSU Server Validation status=" + succeeded + " sessionId=" + sessionId);
+ }
+ if (succeeded) {
+ mProvisioningStateMachine.getHandler().post(() -> {
+ mProvisioningStateMachine.handleServerValidationSuccess(sessionId);
+ });
+ } else {
+ mProvisioningStateMachine.getHandler().post(() -> {
+ mProvisioningStateMachine.handleServerValidationFailure(sessionId);
+ });
+ }
+ }
+
}
}
+
diff --git a/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java b/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java
index 50f2ccf2..366b0a17 100644
--- a/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java
+++ b/com/android/server/wifi/p2p/SupplicantP2pIfaceHal.java
@@ -231,70 +231,133 @@ public class SupplicantP2pIfaceHal {
private boolean initSupplicantP2pIface() {
synchronized (mLock) {
- /** List all supplicant Ifaces */
- final ArrayList<ISupplicant.IfaceInfo> supplicantIfaces = new ArrayList();
- try {
- mISupplicant.listInterfaces((SupplicantStatus status,
- ArrayList<ISupplicant.IfaceInfo> ifaces) -> {
- if (status.code != SupplicantStatusCode.SUCCESS) {
- Log.e(TAG, "Getting Supplicant Interfaces failed: " + status.code);
- return;
- }
- supplicantIfaces.addAll(ifaces);
- });
- } catch (RemoteException e) {
- Log.e(TAG, "ISupplicant.listInterfaces exception: " + e);
+ ISupplicantIface ifaceHwBinder;
+ if (isV1_1()) {
+ ifaceHwBinder = addIfaceV1_1();
+ } else {
+ ifaceHwBinder = getIfaceV1_0();
+ }
+ if (ifaceHwBinder == null) {
+ Log.e(TAG, "initSupplicantP2pIface got null iface");
return false;
}
- if (supplicantIfaces.size() == 0) {
- Log.e(TAG, "Got zero HIDL supplicant ifaces. Stopping supplicant HIDL startup.");
+ mISupplicantP2pIface = getP2pIfaceMockable(ifaceHwBinder);
+ if (!linkToSupplicantP2pIfaceDeath()) {
return false;
}
- SupplicantResult<ISupplicantIface> supplicantIface =
- new SupplicantResult("getInterface()");
- for (ISupplicant.IfaceInfo ifaceInfo : supplicantIfaces) {
- if (ifaceInfo.type == IfaceType.P2P) {
- try {
- mISupplicant.getInterface(ifaceInfo,
- (SupplicantStatus status, ISupplicantIface iface) -> {
+ if (mISupplicantP2pIface != null && mMonitor != null) {
+ // TODO(ender): Get rid of hard-coded interface name, which is
+ // assumed to be the group interface name in several other classes
+ // ("p2p0" should probably become getName()).
+ mCallback = new SupplicantP2pIfaceCallback("p2p0", mMonitor);
+ if (!registerCallback(mCallback)) {
+ Log.e(TAG, "Callback registration failed. Initialization incomplete.");
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private ISupplicantIface getIfaceV1_0() {
+ /** List all supplicant Ifaces */
+ final ArrayList<ISupplicant.IfaceInfo> supplicantIfaces = new ArrayList();
+ try {
+ mISupplicant.listInterfaces((SupplicantStatus status,
+ ArrayList<ISupplicant.IfaceInfo> ifaces) -> {
+ if (status.code != SupplicantStatusCode.SUCCESS) {
+ Log.e(TAG, "Getting Supplicant Interfaces failed: " + status.code);
+ return;
+ }
+ supplicantIfaces.addAll(ifaces);
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "ISupplicant.listInterfaces exception: " + e);
+ return null;
+ }
+ if (supplicantIfaces.size() == 0) {
+ Log.e(TAG, "Got zero HIDL supplicant ifaces. Stopping supplicant HIDL startup.");
+ supplicantServiceDiedHandler();
+ return null;
+ }
+ SupplicantResult<ISupplicantIface> supplicantIface =
+ new SupplicantResult("getInterface()");
+ for (ISupplicant.IfaceInfo ifaceInfo : supplicantIfaces) {
+ if (ifaceInfo.type == IfaceType.P2P) {
+ try {
+ mISupplicant.getInterface(ifaceInfo,
+ (SupplicantStatus status, ISupplicantIface iface) -> {
if (status.code != SupplicantStatusCode.SUCCESS) {
Log.e(TAG, "Failed to get ISupplicantIface " + status.code);
return;
}
supplicantIface.setResult(status, iface);
});
- } catch (RemoteException e) {
- Log.e(TAG, "ISupplicant.getInterface exception: " + e);
- return false;
- }
- break;
+ } catch (RemoteException e) {
+ Log.e(TAG, "ISupplicant.getInterface exception: " + e);
+ supplicantServiceDiedHandler();
+ return null;
}
+ break;
}
+ }
+ return supplicantIface.getResult();
+ }
- if (supplicantIface.getResult() == null) {
- Log.e(TAG, "initSupplicantP2pIface got null iface");
- return false;
- }
- mISupplicantP2pIface = getP2pIfaceMockable(supplicantIface.getResult());
- if (!linkToSupplicantP2pIfaceDeath()) {
- return false;
+ private ISupplicantIface addIfaceV1_1() {
+ synchronized (mLock) {
+ ISupplicant.IfaceInfo ifaceInfo = new ISupplicant.IfaceInfo();
+ ifaceInfo.name = "p2p0";
+ ifaceInfo.type = IfaceType.P2P;
+ SupplicantResult<ISupplicantIface> supplicantIface =
+ new SupplicantResult("addInterface(" + ifaceInfo + ")");
+ try {
+ getSupplicantMockableV1_1().addInterface(ifaceInfo,
+ (SupplicantStatus status, ISupplicantIface iface) -> {
+ if (status.code != SupplicantStatusCode.SUCCESS
+ && status.code != SupplicantStatusCode.FAILURE_IFACE_EXISTS) {
+ Log.e(TAG, "Failed to get ISupplicantIface " + status.code);
+ return;
+ }
+ supplicantIface.setResult(status, iface);
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "ISupplicant.addInterface exception: " + e);
+ supplicantServiceDiedHandler();
+ return null;
}
+ return supplicantIface.getResult();
}
+ }
- if (mISupplicantP2pIface != null && mMonitor != null) {
- // TODO(ender): Get rid of hard-coded interface name, which is
- // assumed to be the group interface name in several other classes
- // ("p2p0" should probably become getName()).
- mCallback = new SupplicantP2pIfaceCallback("p2p0", mMonitor);
- if (!registerCallback(mCallback)) {
- Log.e(TAG, "Callback registration failed. Initialization incomplete.");
+ /**
+ * Teardown the P2P interface.
+ *
+ * @return true on success, false otherwise.
+ */
+ public boolean removeIfaceV1_1() {
+ synchronized (mLock) {
+ try {
+ ISupplicant.IfaceInfo ifaceInfo = new ISupplicant.IfaceInfo();
+ ifaceInfo.name = "p2p0";
+ ifaceInfo.type = IfaceType.P2P;
+ SupplicantStatus status =
+ android.hardware.wifi.supplicant.V1_1.ISupplicant.castFrom(mISupplicant)
+ .removeInterface(ifaceInfo);
+ if (status.code != SupplicantStatusCode.SUCCESS) {
+ Log.e(TAG, "Failed to remove iface " + status.code);
+ return false;
+ }
+ mCallback = null;
+ } catch (RemoteException e) {
+ Log.e(TAG, "ISupplicant.removeInterface exception: " + e);
+ supplicantServiceDiedHandler();
return false;
}
+ mISupplicantP2pIface = null;
+ return true;
}
-
- return true;
}
-
private void supplicantServiceDiedHandler() {
synchronized (mLock) {
mISupplicant = null;
@@ -307,7 +370,9 @@ public class SupplicantP2pIfaceHal {
* Signals whether Initialization completed successfully.
*/
public boolean isInitializationStarted() {
- return mIServiceManager != null;
+ synchronized (mLock) {
+ return mIServiceManager != null;
+ }
}
/**
@@ -329,6 +394,14 @@ public class SupplicantP2pIfaceHal {
return ISupplicant.getService();
}
+ protected android.hardware.wifi.supplicant.V1_1.ISupplicant getSupplicantMockableV1_1()
+ throws RemoteException {
+ synchronized (mLock) {
+ return android.hardware.wifi.supplicant.V1_1.ISupplicant.castFrom(
+ ISupplicant.getService());
+ }
+ }
+
protected ISupplicantP2pIface getP2pIfaceMockable(ISupplicantIface iface) {
return ISupplicantP2pIface.asInterface(iface.asBinder());
}
@@ -337,6 +410,22 @@ public class SupplicantP2pIfaceHal {
return ISupplicantP2pNetwork.asInterface(network.asBinder());
}
+ /**
+ * Check if the device is running V1_1 supplicant service.
+ * @return
+ */
+ private boolean isV1_1() {
+ synchronized (mLock) {
+ try {
+ return (getSupplicantMockableV1_1() != null);
+ } catch (RemoteException e) {
+ Log.e(TAG, "ISupplicant.getService exception: " + e);
+ supplicantServiceDiedHandler();
+ return false;
+ }
+ }
+ }
+
protected static void logd(String s) {
if (DBG) Log.d(TAG, s);
}
@@ -2227,7 +2316,9 @@ public class SupplicantP2pIfaceHal {
}
public boolean isSuccess() {
- return (mStatus != null && mStatus.code == SupplicantStatusCode.SUCCESS);
+ return (mStatus != null
+ && (mStatus.code == SupplicantStatusCode.SUCCESS
+ || mStatus.code == SupplicantStatusCode.FAILURE_IFACE_EXISTS));
}
public E getResult() {
diff --git a/com/android/server/wifi/rtt/RttNative.java b/com/android/server/wifi/rtt/RttNative.java
index dd54c248..d00bf75a 100644
--- a/com/android/server/wifi/rtt/RttNative.java
+++ b/com/android/server/wifi/rtt/RttNative.java
@@ -27,14 +27,13 @@ import android.hardware.wifi.V1_0.RttType;
import android.hardware.wifi.V1_0.WifiChannelWidthInMhz;
import android.hardware.wifi.V1_0.WifiStatus;
import android.hardware.wifi.V1_0.WifiStatusCode;
-import android.net.wifi.ScanResult;
import android.net.wifi.rtt.RangingRequest;
import android.net.wifi.rtt.RangingResult;
+import android.net.wifi.rtt.ResponderConfig;
import android.os.RemoteException;
import android.util.Log;
import com.android.server.wifi.HalDeviceManager;
-import com.android.server.wifi.util.NativeUtil;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -47,6 +46,7 @@ import java.util.List;
public class RttNative extends IWifiRttControllerEventCallback.Stub {
private static final String TAG = "RttNative";
private static final boolean VDBG = false; // STOPSHIP if true
+ /* package */ boolean mDbg = false;
private final RttServiceImpl mRttService;
private final HalDeviceManager mHalDeviceManager;
@@ -84,7 +84,7 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
}
private void updateController() {
- if (VDBG) Log.v(TAG, "updateController: mIWifiRttController=" + mIWifiRttController);
+ if (mDbg) Log.v(TAG, "updateController: mIWifiRttController=" + mIWifiRttController);
// only care about isStarted (Wi-Fi started) not isReady - since if not
// ready then Wi-Fi will also be down.
@@ -111,7 +111,7 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
if (mIWifiRttController == null) {
mRttService.disable();
} else {
- mRttService.enable();
+ mRttService.enableIfPossible();
}
}
}
@@ -125,7 +125,11 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
* @return Success status: true for success, false for failure.
*/
public boolean rangeRequest(int cmdId, RangingRequest request) {
- if (VDBG) Log.v(TAG, "rangeRequest: cmdId=" + cmdId + ", request=" + request);
+ if (mDbg) {
+ Log.v(TAG,
+ "rangeRequest: cmdId=" + cmdId + ", # of requests=" + request.mRttPeers.size());
+ }
+ if (VDBG) Log.v(TAG, "rangeRequest: request=" + request);
synchronized (mLock) {
if (!isReady()) {
Log.e(TAG, "rangeRequest: RttController is null");
@@ -162,7 +166,7 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
* @return Success status: true for success, false for failure.
*/
public boolean rangeCancel(int cmdId, ArrayList<byte[]> macAddresses) {
- if (VDBG) Log.v(TAG, "rangeCancel: cmdId=" + cmdId);
+ if (mDbg) Log.v(TAG, "rangeCancel: cmdId=" + cmdId);
synchronized (mLock) {
if (!isReady()) {
Log.e(TAG, "rangeCancel: RttController is null");
@@ -193,7 +197,7 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
*/
@Override
public void onResults(int cmdId, ArrayList<RttResult> halResults) {
- if (VDBG) Log.v(TAG, "onResults: cmdId=" + cmdId + ", # of results=" + halResults.size());
+ if (mDbg) Log.v(TAG, "onResults: cmdId=" + cmdId + ", # of results=" + halResults.size());
List<RangingResult> results = new ArrayList<>(halResults.size());
mRttService.onRangingResults(cmdId, halResults);
@@ -204,123 +208,116 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
// Skipping any configurations which have an error (printing out a message).
// The caller will only get results for valid configurations.
- for (RangingRequest.RttPeer peer: request.mRttPeers) {
+ for (ResponderConfig responder: request.mRttPeers) {
RttConfig config = new RttConfig();
- if (peer instanceof RangingRequest.RttPeerAp) {
- ScanResult scanResult = ((RangingRequest.RttPeerAp) peer).scanResult;
+ System.arraycopy(responder.macAddress.toByteArray(), 0, config.addr, 0,
+ config.addr.length);
- byte[] addr = NativeUtil.macAddressToByteArray(scanResult.BSSID);
- if (addr.length != config.addr.length) {
- Log.e(TAG, "Invalid configuration: unexpected BSSID length -- " + scanResult);
- continue;
- }
- System.arraycopy(addr, 0, config.addr, 0, config.addr.length);
-
- try {
- config.type =
- scanResult.is80211mcResponder() ? RttType.TWO_SIDED : RttType.ONE_SIDED;
- config.peer = RttPeerType.AP;
- config.channel.width = halChannelWidthFromScanResult(
- scanResult.channelWidth);
- config.channel.centerFreq = scanResult.frequency;
- if (scanResult.centerFreq0 > 0) {
- config.channel.centerFreq0 = scanResult.centerFreq0;
- }
- if (scanResult.centerFreq1 > 0) {
- config.channel.centerFreq1 = scanResult.centerFreq1;
- }
+ try {
+ config.type = responder.supports80211mc ? RttType.TWO_SIDED : RttType.ONE_SIDED;
+ config.peer = halRttPeerTypeFromResponderType(responder.responderType);
+ config.channel.width = halChannelWidthFromResponderChannelWidth(
+ responder.channelWidth);
+ config.channel.centerFreq = responder.frequency;
+ config.channel.centerFreq0 = responder.centerFreq0;
+ config.channel.centerFreq1 = responder.centerFreq1;
+ config.bw = halRttChannelBandwidthFromResponderChannelWidth(responder.channelWidth);
+ config.preamble = halRttPreambleFromResponderPreamble(responder.preamble);
+
+ config.mustRequestLci = false;
+ config.mustRequestLcr = false;
+ if (config.peer == RttPeerType.NAN) {
+ config.burstPeriod = 0;
+ config.numBurst = 0;
+ config.numFramesPerBurst = 5;
+ config.numRetriesPerRttFrame = 3;
+ config.numRetriesPerFtmr = 3;
+ config.burstDuration = 15;
+ } else { // AP + all non-NAN requests
config.burstPeriod = 0;
config.numBurst = 0;
config.numFramesPerBurst = 8;
config.numRetriesPerRttFrame = 0;
config.numRetriesPerFtmr = 0;
- config.mustRequestLci = false;
- config.mustRequestLcr = false;
config.burstDuration = 15;
- config.bw = halChannelBandwidthFromScanResult(scanResult.channelWidth);
- if (config.bw == RttBw.BW_80MHZ || config.bw == RttBw.BW_160MHZ) {
- config.preamble = RttPreamble.VHT;
- } else {
- config.preamble = RttPreamble.HT;
- }
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Invalid configuration: " + e.getMessage());
- continue;
- }
- } else if (peer instanceof RangingRequest.RttPeerAware) {
- RangingRequest.RttPeerAware rttPeerAware = (RangingRequest.RttPeerAware) peer;
-
- if (rttPeerAware.peerMacAddress == null
- || rttPeerAware.peerMacAddress.length != config.addr.length) {
- Log.e(TAG, "Invalid configuration: null MAC or incorrect length");
- continue;
}
- System.arraycopy(rttPeerAware.peerMacAddress, 0, config.addr, 0,
- config.addr.length);
-
- config.type = RttType.TWO_SIDED;
- config.peer = RttPeerType.NAN;
- config.channel.width = WifiChannelWidthInMhz.WIDTH_80;
- config.channel.centerFreq = 5200;
- config.channel.centerFreq0 = 5210;
- config.channel.centerFreq1 = 0;
- config.burstPeriod = 0;
- config.numBurst = 0;
- config.numFramesPerBurst = 5;
- config.numRetriesPerRttFrame = 3;
- config.numRetriesPerFtmr = 3;
- config.mustRequestLci = false;
- config.mustRequestLcr = false;
- config.burstDuration = 15;
- config.preamble = RttPreamble.VHT;
- config.bw = RttBw.BW_80MHZ;
- } else {
- Log.e(TAG, "convertRangingRequestToRttConfigs: unknown request type -- "
- + peer.getClass().getCanonicalName());
- return null;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Invalid configuration: " + e.getMessage());
+ continue;
}
rttConfigs.add(config);
}
-
return rttConfigs;
}
- static int halChannelWidthFromScanResult(int scanResultChannelWidth) {
- switch (scanResultChannelWidth) {
- case ScanResult.CHANNEL_WIDTH_20MHZ:
+ static int halRttPeerTypeFromResponderType(int responderType) {
+ switch (responderType) {
+ case ResponderConfig.RESPONDER_AP:
+ return RttPeerType.AP;
+ case ResponderConfig.RESPONDER_STA:
+ return RttPeerType.STA;
+ case ResponderConfig.RESPONDER_P2P_GO:
+ return RttPeerType.P2P_GO;
+ case ResponderConfig.RESPONDER_P2P_CLIENT:
+ return RttPeerType.P2P_CLIENT;
+ case ResponderConfig.RESPONDER_AWARE:
+ return RttPeerType.NAN;
+ default:
+ throw new IllegalArgumentException(
+ "halRttPeerTypeFromResponderType: bad " + responderType);
+ }
+ }
+
+ static int halChannelWidthFromResponderChannelWidth(int responderChannelWidth) {
+ switch (responderChannelWidth) {
+ case ResponderConfig.CHANNEL_WIDTH_20MHZ:
return WifiChannelWidthInMhz.WIDTH_20;
- case ScanResult.CHANNEL_WIDTH_40MHZ:
+ case ResponderConfig.CHANNEL_WIDTH_40MHZ:
return WifiChannelWidthInMhz.WIDTH_40;
- case ScanResult.CHANNEL_WIDTH_80MHZ:
+ case ResponderConfig.CHANNEL_WIDTH_80MHZ:
return WifiChannelWidthInMhz.WIDTH_80;
- case ScanResult.CHANNEL_WIDTH_160MHZ:
+ case ResponderConfig.CHANNEL_WIDTH_160MHZ:
return WifiChannelWidthInMhz.WIDTH_160;
- case ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ:
+ case ResponderConfig.CHANNEL_WIDTH_80MHZ_PLUS_MHZ:
return WifiChannelWidthInMhz.WIDTH_80P80;
default:
throw new IllegalArgumentException(
- "halChannelWidthFromScanResult: bad " + scanResultChannelWidth);
+ "halChannelWidthFromResponderChannelWidth: bad " + responderChannelWidth);
}
}
- static int halChannelBandwidthFromScanResult(int scanResultChannelWidth) {
- switch (scanResultChannelWidth) {
- case ScanResult.CHANNEL_WIDTH_20MHZ:
+ static int halRttChannelBandwidthFromResponderChannelWidth(int responderChannelWidth) {
+ switch (responderChannelWidth) {
+ case ResponderConfig.CHANNEL_WIDTH_20MHZ:
return RttBw.BW_20MHZ;
- case ScanResult.CHANNEL_WIDTH_40MHZ:
+ case ResponderConfig.CHANNEL_WIDTH_40MHZ:
return RttBw.BW_40MHZ;
- case ScanResult.CHANNEL_WIDTH_80MHZ:
+ case ResponderConfig.CHANNEL_WIDTH_80MHZ:
return RttBw.BW_80MHZ;
- case ScanResult.CHANNEL_WIDTH_160MHZ:
+ case ResponderConfig.CHANNEL_WIDTH_160MHZ:
return RttBw.BW_160MHZ;
- case ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ:
+ case ResponderConfig.CHANNEL_WIDTH_80MHZ_PLUS_MHZ:
return RttBw.BW_160MHZ;
default:
throw new IllegalArgumentException(
- "halChannelBandwidthFromScanResult: bad " + scanResultChannelWidth);
+ "halRttChannelBandwidthFromHalBandwidth: bad " + responderChannelWidth);
+ }
+ }
+
+ static int halRttPreambleFromResponderPreamble(int responderPreamble) {
+ switch (responderPreamble) {
+ case ResponderConfig.PREAMBLE_LEGACY:
+ return RttPreamble.LEGACY;
+ case ResponderConfig.PREAMBLE_HT:
+ return RttPreamble.HT;
+ case ResponderConfig.PREAMBLE_VHT:
+ return RttPreamble.VHT;
+ default:
+ throw new IllegalArgumentException(
+ "halRttPreambleFromResponderPreamble: bad " + responderPreamble);
}
}
diff --git a/com/android/server/wifi/rtt/RttService.java b/com/android/server/wifi/rtt/RttService.java
index 5c2cec15..fabab89f 100644
--- a/com/android/server/wifi/rtt/RttService.java
+++ b/com/android/server/wifi/rtt/RttService.java
@@ -43,14 +43,14 @@ public class RttService extends SystemService {
@Override
public void onStart() {
- Log.i(TAG, "Registering " + Context.WIFI_RTT2_SERVICE);
- publishBinderService(Context.WIFI_RTT2_SERVICE, mImpl);
+ Log.i(TAG, "Registering " + Context.WIFI_RTT_RANGING_SERVICE);
+ publishBinderService(Context.WIFI_RTT_RANGING_SERVICE, mImpl);
}
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
- Log.i(TAG, "Starting " + Context.WIFI_RTT2_SERVICE);
+ Log.i(TAG, "Starting " + Context.WIFI_RTT_RANGING_SERVICE);
WifiInjector wifiInjector = WifiInjector.getInstance();
if (wifiInjector == null) {
@@ -67,7 +67,7 @@ public class RttService extends SystemService {
RttNative rttNative = new RttNative(mImpl, halDeviceManager);
mImpl.start(handlerThread.getLooper(), wifiInjector.getClock(), awareBinder, rttNative,
- wifiPermissionsUtil);
+ wifiPermissionsUtil, wifiInjector.getFrameworkFacade());
}
}
}
diff --git a/com/android/server/wifi/rtt/RttServiceImpl.java b/com/android/server/wifi/rtt/RttServiceImpl.java
index 401daabb..6f025c33 100644
--- a/com/android/server/wifi/rtt/RttServiceImpl.java
+++ b/com/android/server/wifi/rtt/RttServiceImpl.java
@@ -22,17 +22,19 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.database.ContentObserver;
import android.hardware.wifi.V1_0.RttResult;
import android.hardware.wifi.V1_0.RttStatus;
-import android.net.wifi.ScanResult;
+import android.location.LocationManager;
+import android.net.MacAddress;
import android.net.wifi.aware.IWifiAwareMacAddressProvider;
import android.net.wifi.aware.IWifiAwareManager;
-import android.net.wifi.aware.PeerHandle;
import android.net.wifi.rtt.IRttCallback;
import android.net.wifi.rtt.IWifiRttManager;
import android.net.wifi.rtt.RangingRequest;
import android.net.wifi.rtt.RangingResult;
import android.net.wifi.rtt.RangingResultCallback;
+import android.net.wifi.rtt.ResponderConfig;
import android.net.wifi.rtt.WifiRttManager;
import android.os.Binder;
import android.os.Handler;
@@ -42,16 +44,15 @@ import android.os.PowerManager;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.WorkSource;
+import android.provider.Settings;
import android.util.Log;
+import android.util.SparseIntArray;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.WakeupMessage;
import com.android.server.wifi.Clock;
-import com.android.server.wifi.util.NativeUtil;
+import com.android.server.wifi.FrameworkFacade;
import com.android.server.wifi.util.WifiPermissionsUtil;
-import libcore.util.HexEncoding;
-
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -68,6 +69,7 @@ import java.util.Map;
public class RttServiceImpl extends IWifiRttManager.Stub {
private static final String TAG = "RttServiceImpl";
private static final boolean VDBG = false; // STOPSHIP if true
+ private boolean mDbg = false;
private final Context mContext;
private Clock mClock;
@@ -76,17 +78,20 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
private WifiPermissionsUtil mWifiPermissionsUtil;
private ActivityManager mActivityManager;
private PowerManager mPowerManager;
+ private FrameworkFacade mFrameworkFacade;
private RttServiceSynchronized mRttServiceSynchronized;
- @VisibleForTesting
- public static final String HAL_RANGING_TIMEOUT_TAG = TAG + " HAL Ranging Timeout";
+ /* package */ static final String HAL_RANGING_TIMEOUT_TAG = TAG + " HAL Ranging Timeout";
private static final long HAL_RANGING_TIMEOUT_MS = 5_000; // 5 sec
// TODO: b/69323456 convert to a settable value
/* package */ static final long BACKGROUND_PROCESS_EXEC_GAP_MS = 1_800_000; // 30 min
+ // arbitrary, larger than anything reasonable
+ /* package */ static final int MAX_QUEUED_PER_UID = 20;
+
public RttServiceImpl(Context context) {
mContext = context;
}
@@ -103,14 +108,16 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
* @param awareBinder The Wi-Fi Aware service (binder) if supported on the system.
* @param rttNative The Native interface to the HAL.
* @param wifiPermissionsUtil Utility for permission checks.
+ * @param frameworkFacade Facade for framework classes, allows mocking.
*/
public void start(Looper looper, Clock clock, IWifiAwareManager awareBinder,
- RttNative rttNative,
- WifiPermissionsUtil wifiPermissionsUtil) {
+ RttNative rttNative, WifiPermissionsUtil wifiPermissionsUtil,
+ FrameworkFacade frameworkFacade) {
mClock = clock;
mAwareBinder = awareBinder;
mRttNative = rttNative;
mWifiPermissionsUtil = wifiPermissionsUtil;
+ mFrameworkFacade = frameworkFacade;
mRttServiceSynchronized = new RttServiceSynchronized(looper, rttNative);
mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
@@ -121,14 +128,43 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- if (VDBG) Log.v(TAG, "BroadcastReceiver: action=" + action);
+ if (mDbg) Log.v(TAG, "BroadcastReceiver: action=" + action);
if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(action)) {
if (mPowerManager.isDeviceIdleMode()) {
disable();
} else {
- enable();
+ enableIfPossible();
+ }
+ }
+ }
+ }, intentFilter);
+
+ frameworkFacade.registerContentObserver(mContext,
+ Settings.Global.getUriFor(Settings.Global.WIFI_VERBOSE_LOGGING_ENABLED), true,
+ new ContentObserver(mRttServiceSynchronized.mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ enableVerboseLogging(frameworkFacade.getIntegerSetting(mContext,
+ Settings.Global.WIFI_VERBOSE_LOGGING_ENABLED, 0));
}
+ });
+ enableVerboseLogging(frameworkFacade.getIntegerSetting(mContext,
+ Settings.Global.WIFI_VERBOSE_LOGGING_ENABLED, 0));
+
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION);
+ mContext.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mDbg) Log.v(TAG, "onReceive: MODE_CHANGED_ACTION: intent=" + intent);
+ int locationMode = mFrameworkFacade.getSecureIntegerSetting(mContext,
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);
+ if (locationMode == Settings.Secure.LOCATION_MODE_HIGH_ACCURACY
+ || locationMode == Settings.Secure.LOCATION_MODE_BATTERY_SAVING) {
+ enableIfPossible();
+ } else {
+ disable();
}
}
}, intentFilter);
@@ -138,6 +174,18 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
});
}
+ private void enableVerboseLogging(int verbose) {
+ if (verbose > 0) {
+ mDbg = true;
+ } else {
+ mDbg = false;
+ }
+ if (VDBG) {
+ mDbg = true; // just override
+ }
+ mRttNative.mDbg = mDbg;
+ }
+
/*
* ASYNCHRONOUS DOMAIN - can be called from different threads!
*/
@@ -151,10 +199,19 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
}
/**
- * Enable the API: broadcast notification
+ * Enable the API if possible: broadcast notification & start launching any queued requests
+ *
+ * If possible:
+ * - RTT HAL is available
+ * - Not in Idle mode
+ * - Location Mode allows Wi-Fi based locationing
*/
- public void enable() {
- if (VDBG) Log.v(TAG, "enable");
+ public void enableIfPossible() {
+ boolean isAvailable = isAvailable();
+ if (VDBG) Log.v(TAG, "enableIfPossible: isAvailable=" + isAvailable);
+ if (!isAvailable) {
+ return;
+ }
sendRttStateChangedBroadcast(true);
mRttServiceSynchronized.mHandler.post(() -> {
// queue should be empty at this point (but this call allows validation)
@@ -181,7 +238,11 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
*/
@Override
public boolean isAvailable() {
- return mRttNative.isReady() && !mPowerManager.isDeviceIdleMode();
+ int locationMode = mFrameworkFacade.getSecureIntegerSetting(mContext,
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);
+ return mRttNative.isReady() && !mPowerManager.isDeviceIdleMode() && (
+ locationMode == Settings.Secure.LOCATION_MODE_HIGH_ACCURACY
+ || locationMode == Settings.Secure.LOCATION_MODE_BATTERY_SAVING);
}
/**
@@ -222,7 +283,7 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
// permission checks
enforceAccessPermission();
enforceChangePermission();
- enforceLocationPermission(callingPackage, uid);
+ mWifiPermissionsUtil.enforceLocationPermission(callingPackage, uid);
if (workSource != null) {
enforceLocationHardware();
}
@@ -231,7 +292,7 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
IBinder.DeathRecipient dr = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
- if (VDBG) Log.v(TAG, "binderDied: uid=" + uid);
+ if (mDbg) Log.v(TAG, "binderDied: uid=" + uid);
binder.unlinkToDeath(this, 0);
mRttServiceSynchronized.mHandler.post(() -> {
@@ -291,11 +352,6 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CHANGE_WIFI_STATE, TAG);
}
- private void enforceLocationPermission(String callingPackage, int uid) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION,
- TAG);
- }
-
private void enforceLocationHardware() {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.LOCATION_HARDWARE,
TAG);
@@ -349,24 +405,8 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
private void cancelRanging(RttRequestInfo rri) {
ArrayList<byte[]> macAddresses = new ArrayList<>();
- for (RangingRequest.RttPeer peer : rri.request.mRttPeers) {
- if (peer instanceof RangingRequest.RttPeerAp) {
- ScanResult scanResult =
- ((RangingRequest.RttPeerAp) peer).scanResult;
-
- byte[] addr = NativeUtil.macAddressToByteArray(scanResult.BSSID);
- if (addr.length != 6) {
- Log.e(TAG, "Invalid configuration: unexpected BSSID length -- "
- + peer);
- continue;
- }
- macAddresses.add(addr);
- } else if (peer instanceof RangingRequest.RttPeerAware) {
- if (((RangingRequest.RttPeerAware) peer).peerMacAddress != null) {
- macAddresses.add(
- ((RangingRequest.RttPeerAware) peer).peerMacAddress);
- }
- }
+ for (ResponderConfig peer : rri.request.mRttPeers) {
+ macAddresses.add(peer.macAddress.toByteArray());
}
mRttNative.rangeCancel(rri.cmdId, macAddresses);
@@ -477,6 +517,19 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
private void queueRangingRequest(int uid, WorkSource workSource, IBinder binder,
IBinder.DeathRecipient dr, String callingPackage, RangingRequest request,
IRttCallback callback) {
+ if (isRequestorSpamming(workSource)) {
+ Log.w(TAG,
+ "Work source " + workSource + " is spamming, dropping request: " + request);
+ binder.unlinkToDeath(dr, 0);
+ try {
+ callback.onRangingFailure(RangingResultCallback.STATUS_CODE_FAIL);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RttServiceSynchronized.queueRangingRequest: spamming, callback "
+ + "failed -- " + e);
+ }
+ return;
+ }
+
RttRequestInfo newRequest = new RttRequestInfo();
newRequest.uid = uid;
newRequest.workSource = workSource;
@@ -494,6 +547,30 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
executeNextRangingRequestIfPossible(false);
}
+ private boolean isRequestorSpamming(WorkSource ws) {
+ if (VDBG) Log.v(TAG, "isRequestorSpamming: ws" + ws);
+
+ SparseIntArray counts = new SparseIntArray();
+
+ for (RttRequestInfo rri : mRttRequestQueue) {
+ for (int i = 0; i < rri.workSource.size(); ++i) {
+ int uid = rri.workSource.get(i);
+ counts.put(uid, counts.get(uid) + 1);
+ }
+ }
+
+ for (int i = 0; i < ws.size(); ++i) {
+ if (counts.get(ws.get(i)) < MAX_QUEUED_PER_UID) {
+ return false;
+ }
+ }
+
+ if (mDbg) {
+ Log.v(TAG, "isRequestorSpamming: ws=" + ws + ", someone is spamming: " + counts);
+ }
+ return true;
+ }
+
private void executeNextRangingRequestIfPossible(boolean popFirst) {
if (VDBG) Log.v(TAG, "executeNextRangingRequestIfPossible: popFirst=" + popFirst);
@@ -584,7 +661,7 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
/**
* Perform pre-execution throttling checks:
* - If all uids in ws are in background then check last execution and block if request is
- * more frequent than permitted
+ * more frequent than permitted
* - If executing (i.e. permitted) then update execution time
*
* Returns true to permit execution, false to abort it.
@@ -652,12 +729,9 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
*/
private boolean processAwarePeerHandles(RttRequestInfo request) {
List<Integer> peerIdsNeedingTranslation = new ArrayList<>();
- for (RangingRequest.RttPeer rttPeer: request.request.mRttPeers) {
- if (rttPeer instanceof RangingRequest.RttPeerAware) {
- RangingRequest.RttPeerAware awarePeer = (RangingRequest.RttPeerAware) rttPeer;
- if (awarePeer.peerHandle != null && awarePeer.peerMacAddress == null) {
- peerIdsNeedingTranslation.add(awarePeer.peerHandle.peerId);
- }
+ for (ResponderConfig rttPeer : request.request.mRttPeers) {
+ if (rttPeer.peerHandle != null && rttPeer.macAddress == null) {
+ peerIdsNeedingTranslation.add(rttPeer.peerHandle.peerId);
}
}
@@ -713,14 +787,19 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
+ ", peerIdToMacMap=" + peerIdToMacMap);
}
- for (RangingRequest.RttPeer rttPeer: request.request.mRttPeers) {
- if (rttPeer instanceof RangingRequest.RttPeerAware) {
- RangingRequest.RttPeerAware awarePeer = (RangingRequest.RttPeerAware) rttPeer;
- if (awarePeer.peerHandle != null && awarePeer.peerMacAddress == null) {
- awarePeer.peerMacAddress = peerIdToMacMap.get(awarePeer.peerHandle.peerId);
- }
+ RangingRequest.Builder newRequestBuilder = new RangingRequest.Builder();
+ for (ResponderConfig rttPeer : request.request.mRttPeers) {
+ if (rttPeer.peerHandle != null && rttPeer.macAddress == null) {
+ newRequestBuilder.addResponder(new ResponderConfig(
+ MacAddress.fromBytes(peerIdToMacMap.get(rttPeer.peerHandle.peerId)),
+ rttPeer.peerHandle, rttPeer.responderType, rttPeer.supports80211mc,
+ rttPeer.channelWidth, rttPeer.frequency, rttPeer.centerFreq0,
+ rttPeer.centerFreq1, rttPeer.preamble));
+ } else {
+ newRequestBuilder.addResponder(rttPeer);
}
}
+ request.request = newRequestBuilder.build();
// run request again
startRanging(request);
@@ -749,6 +828,12 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
boolean permissionGranted = mWifiPermissionsUtil.checkCallersLocationPermission(
topOfQueueRequest.callingPackage, topOfQueueRequest.uid);
+ int locationMode = mFrameworkFacade.getSecureIntegerSetting(mContext,
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);
+ if (locationMode != Settings.Secure.LOCATION_MODE_HIGH_ACCURACY
+ && locationMode != Settings.Secure.LOCATION_MODE_BATTERY_SAVING) {
+ permissionGranted = false;
+ }
try {
if (permissionGranted) {
List<RangingResult> finalResults = postProcessResults(topOfQueueRequest.request,
@@ -780,52 +865,43 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
*/
private List<RangingResult> postProcessResults(RangingRequest request,
List<RttResult> results) {
- Map<String, RttResult> resultEntries = new HashMap<>();
- for (RttResult result: results) {
- resultEntries.put(new String(HexEncoding.encode(result.addr)), result);
+ Map<MacAddress, RttResult> resultEntries = new HashMap<>();
+ for (RttResult result : results) {
+ resultEntries.put(MacAddress.fromBytes(result.addr), result);
}
List<RangingResult> finalResults = new ArrayList<>(request.mRttPeers.size());
- for (RangingRequest.RttPeer peer: request.mRttPeers) {
- byte[] addr;
- if (peer instanceof RangingRequest.RttPeerAp) {
- addr = NativeUtil.macAddressToByteArray(
- ((RangingRequest.RttPeerAp) peer).scanResult.BSSID);
- } else if (peer instanceof RangingRequest.RttPeerAware) {
- addr = ((RangingRequest.RttPeerAware) peer).peerMacAddress;
- } else {
- Log.w(TAG, "postProcessResults: unknown peer type -- " + peer.getClass());
- continue;
- }
-
- RttResult resultForRequest = resultEntries.get(
- new String(HexEncoding.encode(addr)));
+ for (ResponderConfig peer : request.mRttPeers) {
+ RttResult resultForRequest = resultEntries.get(peer.macAddress);
if (resultForRequest == null) {
- if (VDBG) {
- Log.v(TAG, "postProcessResults: missing=" + new String(
- HexEncoding.encode(addr)));
+ if (mDbg) {
+ Log.v(TAG, "postProcessResults: missing=" + peer.macAddress);
+ }
+ if (peer.peerHandle == null) {
+ finalResults.add(
+ new RangingResult(RangingResult.STATUS_FAIL, peer.macAddress, 0, 0,
+ 0, 0));
+ } else {
+ finalResults.add(
+ new RangingResult(RangingResult.STATUS_FAIL, peer.peerHandle, 0, 0,
+ 0, 0));
}
- finalResults.add(
- new RangingResult(RangingResult.STATUS_FAIL, addr, 0, 0, 0, 0));
} else {
int status = resultForRequest.status == RttStatus.SUCCESS
? RangingResult.STATUS_SUCCESS : RangingResult.STATUS_FAIL;
- PeerHandle peerHandle = null;
- if (peer instanceof RangingRequest.RttPeerAware) {
- peerHandle = ((RangingRequest.RttPeerAware) peer).peerHandle;
- }
-
- if (peerHandle == null) {
+ if (peer.peerHandle == null) {
finalResults.add(
- new RangingResult(status, addr, resultForRequest.distanceInMm,
- resultForRequest.distanceSdInMm, resultForRequest.rssi,
- resultForRequest.timeStampInUs));
+ new RangingResult(status, peer.macAddress,
+ resultForRequest.distanceInMm,
+ resultForRequest.distanceSdInMm,
+ resultForRequest.rssi, resultForRequest.timeStampInUs));
} else {
finalResults.add(
- new RangingResult(status, peerHandle, resultForRequest.distanceInMm,
- resultForRequest.distanceSdInMm, resultForRequest.rssi,
- resultForRequest.timeStampInUs));
+ new RangingResult(status, peer.peerHandle,
+ resultForRequest.distanceInMm,
+ resultForRequest.distanceSdInMm,
+ resultForRequest.rssi, resultForRequest.timeStampInUs));
}
}
}
diff --git a/com/android/server/wifi/scanner/WifiScanningServiceImpl.java b/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
index ed1bedfa..444c0e0e 100644
--- a/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
+++ b/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
@@ -16,13 +16,15 @@
package com.android.server.wifi.scanner;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
import android.Manifest;
import android.app.AlarmManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.PackageManager;
import android.net.wifi.IWifiScanner;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
@@ -721,6 +723,20 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
return false;
}
}
+ if (mContext.checkPermission(
+ Manifest.permission.NETWORK_STACK, UNKNOWN_PID, ci.getUid())
+ == PERMISSION_DENIED) {
+ if (!ArrayUtils.isEmpty(settings.hiddenNetworks)) {
+ Log.e(TAG, "Failing single scan because app " + ci.getUid()
+ + " does not have permission to set hidden networks");
+ return false;
+ }
+ if (settings.type != WifiScanner.TYPE_LOW_LATENCY) {
+ Log.e(TAG, "Failing single scan because app " + ci.getUid()
+ + " does not have permission to set type");
+ return false;
+ }
+ }
return true;
}
@@ -1963,7 +1979,7 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
- != PackageManager.PERMISSION_GRANTED) {
+ != PERMISSION_GRANTED) {
pw.println("Permission Denial: can't dump WifiScanner from from pid="
+ Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid()
diff --git a/com/android/server/wifi/scanner/WificondScannerImpl.java b/com/android/server/wifi/scanner/WificondScannerImpl.java
index 72a7a27e..9afe061d 100644
--- a/com/android/server/wifi/scanner/WificondScannerImpl.java
+++ b/com/android/server/wifi/scanner/WificondScannerImpl.java
@@ -35,7 +35,6 @@ import com.android.server.wifi.scanner.ChannelHelper.ChannelCollection;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -66,28 +65,16 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
private final Object mSettingsLock = new Object();
- // Next scan settings to apply when the previous scan completes
- private WifiNative.ScanSettings mPendingSingleScanSettings = null;
- private WifiNative.ScanEventHandler mPendingSingleScanEventHandler = null;
-
private ArrayList<ScanDetail> mNativeScanResults;
private WifiScanner.ScanData mLatestSingleScanResult =
new WifiScanner.ScanData(0, 0, new ScanResult[0]);
- // Settings for the currently running scan, null if no scan active
+ // Settings for the currently running single scan, null if no scan active
private LastScanSettings mLastScanSettings = null;
+ // Settings for the currently running pno scan, null if no scan active
+ private LastPnoScanSettings mLastPnoScanSettings = null;
- // Pno related info.
- private WifiNative.PnoSettings mPnoSettings = null;
- private WifiNative.PnoEventHandler mPnoEventHandler;
private final boolean mHwPnoScanSupported;
- private final HwPnoDebouncer mHwPnoDebouncer;
- private final HwPnoDebouncer.Listener mHwPnoDebouncerListener = new HwPnoDebouncer.Listener() {
- public void onPnoScanFailed() {
- Log.e(TAG, "Pno scan failure received");
- reportPnoScanFailure();
- }
- };
/**
* Duration to wait before timing out a scan.
@@ -114,7 +101,6 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
mEventHandler = new Handler(looper, this);
mClock = clock;
- mHwPnoDebouncer = new HwPnoDebouncer(mWifiNative, mAlarmManager, mEventHandler, mClock);
// Check if the device supports HW PNO scans.
mHwPnoScanSupported = mContext.getResources().getBoolean(
@@ -131,10 +117,9 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
@Override
public void cleanup() {
synchronized (mSettingsLock) {
- mPendingSingleScanSettings = null;
- mPendingSingleScanEventHandler = null;
stopHwPnoScan();
mLastScanSettings = null; // finally clear any active scan
+ mLastPnoScanSettings = null; // finally clear any active scan
}
}
@@ -162,14 +147,65 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
return false;
}
synchronized (mSettingsLock) {
- if (mPendingSingleScanSettings != null
- || (mLastScanSettings != null && mLastScanSettings.singleScanActive)) {
+ if (mLastScanSettings != null) {
Log.w(TAG, "A single scan is already running");
return false;
}
- mPendingSingleScanSettings = settings;
- mPendingSingleScanEventHandler = eventHandler;
- processPendingScans();
+
+ ChannelCollection allFreqs = mChannelHelper.createChannelCollection();
+ boolean reportFullResults = false;
+
+ for (int i = 0; i < settings.num_buckets; ++i) {
+ WifiNative.BucketSettings bucketSettings = settings.buckets[i];
+ if ((bucketSettings.report_events
+ & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) {
+ reportFullResults = true;
+ }
+ allFreqs.addChannels(bucketSettings);
+ }
+
+ Set<String> hiddenNetworkSSIDSet = new HashSet<>();
+ if (settings.hiddenNetworks != null) {
+ int numHiddenNetworks =
+ Math.min(settings.hiddenNetworks.length, MAX_HIDDEN_NETWORK_IDS_PER_SCAN);
+ for (int i = 0; i < numHiddenNetworks; i++) {
+ hiddenNetworkSSIDSet.add(settings.hiddenNetworks[i].ssid);
+ }
+ }
+ mLastScanSettings = new LastScanSettings(
+ mClock.getElapsedSinceBootMillis(),
+ reportFullResults, allFreqs, eventHandler);
+
+ boolean success = false;
+ Set<Integer> freqs;
+ if (!allFreqs.isEmpty()) {
+ freqs = allFreqs.getScanFreqs();
+ success = mWifiNative.scan(freqs, hiddenNetworkSSIDSet);
+ if (!success) {
+ Log.e(TAG, "Failed to start scan, freqs=" + freqs);
+ }
+ } else {
+ // There is a scan request but no available channels could be scanned for.
+ // We regard it as a scan failure in this case.
+ Log.e(TAG, "Failed to start scan because there is no available channel to scan");
+ }
+ if (success) {
+ if (DBG) {
+ Log.d(TAG, "Starting wifi scan for freqs=" + freqs);
+ }
+
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ mClock.getElapsedSinceBootMillis() + SCAN_TIMEOUT_MS,
+ TIMEOUT_ALARM_TAG, mScanTimeoutListener, mEventHandler);
+ } else {
+ // indicate scan failure async
+ mEventHandler.post(new Runnable() {
+ public void run() {
+ reportScanFailure();
+ }
+ });
+ }
+
return true;
}
}
@@ -204,123 +240,6 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
private void handleScanTimeout() {
Log.e(TAG, "Timed out waiting for scan result from wificond");
reportScanFailure();
- processPendingScans();
- }
-
- private boolean isDifferentPnoScanSettings(LastScanSettings newScanSettings) {
- return (mLastScanSettings == null || !Arrays.equals(
- newScanSettings.pnoNetworkList, mLastScanSettings.pnoNetworkList));
- }
-
- private void processPendingScans() {
- synchronized (mSettingsLock) {
- // Wait for the active scan result to come back to reschedule other scans,
- // unless if HW pno scan is running. Hw PNO scans are paused it if there
- // are other pending scans,
- if (mLastScanSettings != null && !mLastScanSettings.hwPnoScanActive) {
- return;
- }
-
- ChannelCollection allFreqs = mChannelHelper.createChannelCollection();
- Set<String> hiddenNetworkSSIDSet = new HashSet<>();
- final LastScanSettings newScanSettings =
- new LastScanSettings(mClock.getElapsedSinceBootMillis());
-
- if (mPendingSingleScanSettings != null) {
- boolean reportFullResults = false;
- ChannelCollection singleScanFreqs = mChannelHelper.createChannelCollection();
- for (int i = 0; i < mPendingSingleScanSettings.num_buckets; ++i) {
- WifiNative.BucketSettings bucketSettings =
- mPendingSingleScanSettings.buckets[i];
- if ((bucketSettings.report_events
- & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) {
- reportFullResults = true;
- }
- singleScanFreqs.addChannels(bucketSettings);
- allFreqs.addChannels(bucketSettings);
- }
- newScanSettings.setSingleScan(reportFullResults, singleScanFreqs,
- mPendingSingleScanEventHandler);
-
- WifiNative.HiddenNetwork[] hiddenNetworks =
- mPendingSingleScanSettings.hiddenNetworks;
- if (hiddenNetworks != null) {
- int numHiddenNetworks =
- Math.min(hiddenNetworks.length, MAX_HIDDEN_NETWORK_IDS_PER_SCAN);
- for (int i = 0; i < numHiddenNetworks; i++) {
- hiddenNetworkSSIDSet.add(hiddenNetworks[i].ssid);
- }
- }
-
- mPendingSingleScanSettings = null;
- mPendingSingleScanEventHandler = null;
- }
-
- if (newScanSettings.singleScanActive) {
- boolean success = false;
- Set<Integer> freqs;
- if (!allFreqs.isEmpty()) {
- pauseHwPnoScan();
- freqs = allFreqs.getScanFreqs();
- success = mWifiNative.scan(freqs, hiddenNetworkSSIDSet);
- if (!success) {
- Log.e(TAG, "Failed to start scan, freqs=" + freqs);
- }
- } else {
- // There is a scan request but no available channels could be scanned for.
- // We regard it as a scan failure in this case.
- Log.e(TAG, "Failed to start scan because there is "
- + "no available channel to scan for");
- }
- if (success) {
- // TODO handle scan timeout
- if (DBG) {
- Log.d(TAG, "Starting wifi scan for freqs=" + freqs
- + ", single=" + newScanSettings.singleScanActive);
- }
- mLastScanSettings = newScanSettings;
- mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- mClock.getElapsedSinceBootMillis() + SCAN_TIMEOUT_MS,
- TIMEOUT_ALARM_TAG, mScanTimeoutListener, mEventHandler);
- } else {
- // indicate scan failure async
- mEventHandler.post(new Runnable() {
- public void run() {
- if (newScanSettings.singleScanEventHandler != null) {
- newScanSettings.singleScanEventHandler
- .onScanStatus(WifiNative.WIFI_SCAN_FAILED);
- }
- }
- });
- }
- } else if (isHwPnoScanRequired()) {
- newScanSettings.setHwPnoScan(mPnoSettings.networkList, mPnoEventHandler);
- boolean status;
- // If the PNO network list has changed from the previous request, ensure that
- // we bypass the debounce logic and restart PNO scan.
- if (isDifferentPnoScanSettings(newScanSettings)) {
- status = restartHwPnoScan(mPnoSettings);
- } else {
- status = startHwPnoScan(mPnoSettings);
- }
- if (status) {
- mLastScanSettings = newScanSettings;
- } else {
- Log.e(TAG, "Failed to start PNO scan");
- // indicate scan failure async
- mEventHandler.post(new Runnable() {
- public void run() {
- if (mPnoEventHandler != null) {
- mPnoEventHandler.onPnoScanFailed();
- }
- // Clean up PNO state, we don't want to continue PNO scanning.
- mPnoSettings = null;
- mPnoEventHandler = null;
- }
- });
- }
- }
- }
}
@Override
@@ -330,16 +249,13 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
Log.w(TAG, "Scan failed");
mAlarmManager.cancel(mScanTimeoutListener);
reportScanFailure();
- processPendingScans();
break;
case WifiMonitor.PNO_SCAN_RESULTS_EVENT:
pollLatestScanDataForPno();
- processPendingScans();
break;
case WifiMonitor.SCAN_RESULTS_EVENT:
mAlarmManager.cancel(mScanTimeoutListener);
pollLatestScanData();
- processPendingScans();
break;
default:
// ignore unknown event
@@ -361,21 +277,19 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
private void reportPnoScanFailure() {
synchronized (mSettingsLock) {
- if (mLastScanSettings != null && mLastScanSettings.hwPnoScanActive) {
- if (mLastScanSettings.pnoScanEventHandler != null) {
- mLastScanSettings.pnoScanEventHandler.onPnoScanFailed();
+ if (mLastPnoScanSettings != null) {
+ if (mLastPnoScanSettings.pnoScanEventHandler != null) {
+ mLastPnoScanSettings.pnoScanEventHandler.onPnoScanFailed();
}
// Clean up PNO state, we don't want to continue PNO scanning.
- mPnoSettings = null;
- mPnoEventHandler = null;
- mLastScanSettings = null;
+ mLastPnoScanSettings = null;
}
}
}
private void pollLatestScanDataForPno() {
synchronized (mSettingsLock) {
- if (mLastScanSettings == null) {
+ if (mLastPnoScanSettings == null) {
// got a scan before we started scanning or after scan was canceled
return;
}
@@ -385,10 +299,8 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
for (int i = 0; i < mNativeScanResults.size(); ++i) {
ScanResult result = mNativeScanResults.get(i).getScanResult();
long timestamp_ms = result.timestamp / 1000; // convert us -> ms
- if (timestamp_ms > mLastScanSettings.startTime) {
- if (mLastScanSettings.hwPnoScanActive) {
- hwPnoScanResults.add(result);
- }
+ if (timestamp_ms > mLastPnoScanSettings.startTime) {
+ hwPnoScanResults.add(result);
} else {
numFilteredScanResults++;
}
@@ -398,25 +310,11 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
Log.d(TAG, "Filtering out " + numFilteredScanResults + " pno scan results.");
}
- if (mLastScanSettings.hwPnoScanActive
- && mLastScanSettings.pnoScanEventHandler != null) {
+ if (mLastPnoScanSettings.pnoScanEventHandler != null) {
ScanResult[] pnoScanResultsArray =
hwPnoScanResults.toArray(new ScanResult[hwPnoScanResults.size()]);
- mLastScanSettings.pnoScanEventHandler.onPnoNetworkFound(pnoScanResultsArray);
+ mLastPnoScanSettings.pnoScanEventHandler.onPnoNetworkFound(pnoScanResultsArray);
}
- // On pno scan result event, we are expecting a mLastScanSettings for pno scan.
- // However, if unlikey mLastScanSettings is for single scan, we need this part
- // to protect from leaving WifiSingleScanStateMachine in a forever wait state.
- if (mLastScanSettings.singleScanActive
- && mLastScanSettings.singleScanEventHandler != null) {
- Log.w(TAG, "Polling pno scan result when single scan is active, reporting"
- + " single scan failure");
- mLastScanSettings.singleScanEventHandler
- .onScanStatus(WifiNative.WIFI_SCAN_FAILED);
- }
- // mLastScanSettings is for either single/batched scan or pno scan.
- // We can safely set it to null when pno scan finishes.
- mLastScanSettings = null;
}
}
@@ -445,8 +343,7 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
ScanResult result = mNativeScanResults.get(i).getScanResult();
long timestamp_ms = result.timestamp / 1000; // convert us -> ms
if (timestamp_ms > mLastScanSettings.startTime) {
- if (mLastScanSettings.singleScanActive
- && mLastScanSettings.singleScanFreqs.containsChannel(
+ if (mLastScanSettings.singleScanFreqs.containsChannel(
result.frequency)) {
singleScanResults.add(result);
}
@@ -458,8 +355,7 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
Log.d(TAG, "Filtering out " + numFilteredScanResults + " scan results.");
}
- if (mLastScanSettings.singleScanActive
- && mLastScanSettings.singleScanEventHandler != null) {
+ if (mLastScanSettings.singleScanEventHandler != null) {
if (mLastScanSettings.reportSingleScanFullResults) {
for (ScanResult scanResult : singleScanResults) {
// ignore buckets scanned since there is only one bucket for a single scan
@@ -486,20 +382,11 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
}
private boolean startHwPnoScan(WifiNative.PnoSettings pnoSettings) {
- return mHwPnoDebouncer.startPnoScan(pnoSettings, mHwPnoDebouncerListener);
+ return mWifiNative.startPnoScan(pnoSettings);
}
private void stopHwPnoScan() {
- mHwPnoDebouncer.stopPnoScan();
- }
-
- private void pauseHwPnoScan() {
- mHwPnoDebouncer.forceStopPnoScan();
- }
-
- private boolean restartHwPnoScan(WifiNative.PnoSettings pnoSettings) {
- mHwPnoDebouncer.forceStopPnoScan();
- return mHwPnoDebouncer.startPnoScan(pnoSettings, mHwPnoDebouncerListener);
+ mWifiNative.stopPnoScan();
}
/**
@@ -511,26 +398,27 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
return (!isConnectedPno & mHwPnoScanSupported);
}
- private boolean isHwPnoScanRequired() {
- synchronized (mSettingsLock) {
- if (mPnoSettings == null) return false;
- return isHwPnoScanRequired(mPnoSettings.isConnected);
- }
- }
-
@Override
public boolean setHwPnoList(WifiNative.PnoSettings settings,
WifiNative.PnoEventHandler eventHandler) {
synchronized (mSettingsLock) {
- if (mPnoSettings != null) {
+ if (mLastPnoScanSettings != null) {
Log.w(TAG, "Already running a PNO scan");
return false;
}
- mPnoEventHandler = eventHandler;
- mPnoSettings = settings;
+ if (!isHwPnoScanRequired(settings.isConnected)) {
+ return false;
+ }
+
+ if (startHwPnoScan(settings)) {
+ mLastPnoScanSettings = new LastPnoScanSettings(
+ mClock.getElapsedSinceBootMillis(),
+ settings.networkList, eventHandler);
- // For wificond based PNO, we start the scan immediately when we set pno list.
- processPendingScans();
+ } else {
+ Log.e(TAG, "Failed to start PNO scan");
+ reportPnoScanFailure();
+ }
return true;
}
}
@@ -538,12 +426,11 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
@Override
public boolean resetHwPnoList() {
synchronized (mSettingsLock) {
- if (mPnoSettings == null) {
+ if (mLastPnoScanSettings == null) {
Log.w(TAG, "No PNO scan running");
return false;
}
- mPnoEventHandler = null;
- mPnoSettings = null;
+ mLastPnoScanSettings = null;
// For wificond based PNO, we stop the scan immediately when we reset pno list.
stopHwPnoScan();
return true;
@@ -591,204 +478,36 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
}
private static class LastScanSettings {
- public long startTime;
-
- LastScanSettings(long startTime) {
- this.startTime = startTime;
- }
-
- // Single scan settings
- public boolean singleScanActive = false;
- public boolean reportSingleScanFullResults;
- public ChannelCollection singleScanFreqs;
- public WifiNative.ScanEventHandler singleScanEventHandler;
-
- public void setSingleScan(boolean reportSingleScanFullResults,
+ LastScanSettings(long startTime,
+ boolean reportSingleScanFullResults,
ChannelCollection singleScanFreqs,
WifiNative.ScanEventHandler singleScanEventHandler) {
- singleScanActive = true;
+ this.startTime = startTime;
this.reportSingleScanFullResults = reportSingleScanFullResults;
this.singleScanFreqs = singleScanFreqs;
this.singleScanEventHandler = singleScanEventHandler;
}
- public boolean hwPnoScanActive = false;
- public WifiNative.PnoNetwork[] pnoNetworkList;
- public WifiNative.PnoEventHandler pnoScanEventHandler;
+ public long startTime;
+ public boolean reportSingleScanFullResults;
+ public ChannelCollection singleScanFreqs;
+ public WifiNative.ScanEventHandler singleScanEventHandler;
- public void setHwPnoScan(
+ }
+
+ private static class LastPnoScanSettings {
+ LastPnoScanSettings(long startTime,
WifiNative.PnoNetwork[] pnoNetworkList,
WifiNative.PnoEventHandler pnoScanEventHandler) {
- hwPnoScanActive = true;
+ this.startTime = startTime;
this.pnoNetworkList = pnoNetworkList;
this.pnoScanEventHandler = pnoScanEventHandler;
}
- }
-
- /**
- * HW PNO Debouncer is used to debounce PNO requests. This guards against toggling the PNO
- * state too often which is not handled very well by some drivers.
- * Note: This is not thread safe!
- */
- public static class HwPnoDebouncer {
- public static final String PNO_DEBOUNCER_ALARM_TAG = TAG + "Pno Monitor";
- private static final int MINIMUM_PNO_GAP_MS = 5 * 1000;
-
- private final WifiNative mWifiNative;
- private final AlarmManager mAlarmManager;
- private final Handler mEventHandler;
- private final Clock mClock;
- private long mLastPnoChangeTimeStamp = -1L;
- private boolean mExpectedPnoState = false;
- private boolean mCurrentPnoState = false;;
- private boolean mWaitForTimer = false;
- private Listener mListener;
- private WifiNative.PnoSettings mPnoSettings;
-
- /**
- * Interface used to indicate PNO scan notifications.
- */
- public interface Listener {
- /**
- * Used to indicate a delayed PNO scan request failure.
- */
- void onPnoScanFailed();
- }
-
- public HwPnoDebouncer(WifiNative wifiNative, AlarmManager alarmManager,
- Handler eventHandler, Clock clock) {
- mWifiNative = wifiNative;
- mAlarmManager = alarmManager;
- mEventHandler = eventHandler;
- mClock = clock;
- }
- /**
- * Enable PNO state in wificond
- */
- private boolean startPnoScanInternal() {
- if (mCurrentPnoState) {
- if (DBG) Log.d(TAG, "PNO state is already enable");
- return true;
- }
- if (mPnoSettings == null) {
- Log.e(TAG, "PNO state change to enable failed, no available Pno settings");
- return false;
- }
- mLastPnoChangeTimeStamp = mClock.getElapsedSinceBootMillis();
- Log.d(TAG, "Remove all networks from supplicant before starting PNO scan");
- mWifiNative.removeAllNetworks();
- if (mWifiNative.startPnoScan(mPnoSettings)) {
- Log.d(TAG, "Changed PNO state from " + mCurrentPnoState + " to enable");
- mCurrentPnoState = true;
- return true;
- } else {
- Log.e(TAG, "PNO state change to enable failed");
- mCurrentPnoState = false;
- }
- return false;
- }
-
- /**
- * Disable PNO state in wificond
- */
- private boolean stopPnoScanInternal() {
- if (!mCurrentPnoState) {
- if (DBG) Log.d(TAG, "PNO state is already disable");
- return true;
- }
- mLastPnoChangeTimeStamp = mClock.getElapsedSinceBootMillis();
- if (mWifiNative.stopPnoScan()) {
- Log.d(TAG, "Changed PNO state from " + mCurrentPnoState + " to disable");
- mCurrentPnoState = false;
- return true;
- } else {
- Log.e(TAG, "PNO state change to disable failed");
- mCurrentPnoState = false;
- }
- return false;
- }
-
- private final AlarmManager.OnAlarmListener mAlarmListener =
- new AlarmManager.OnAlarmListener() {
- public void onAlarm() {
- if (DBG) Log.d(TAG, "PNO timer expired, expected state " + mExpectedPnoState);
- if (mExpectedPnoState) {
- if (!startPnoScanInternal()) {
- if (mListener != null) {
- mListener.onPnoScanFailed();
- }
- }
- } else {
- stopPnoScanInternal();
- }
- mWaitForTimer = false;
- }
- };
-
- /**
- * Enable/Disable PNO state. This method will debounce PNO scan requests.
- * @param enable boolean indicating whether PNO is being enabled or disabled.
- */
- private boolean setPnoState(boolean enable) {
- boolean isSuccess = true;
- mExpectedPnoState = enable;
- if (!mWaitForTimer) {
- long timeDifference = mClock.getElapsedSinceBootMillis() - mLastPnoChangeTimeStamp;
- if (timeDifference >= MINIMUM_PNO_GAP_MS) {
- if (enable) {
- isSuccess = startPnoScanInternal();
- } else {
- isSuccess = stopPnoScanInternal();
- }
- } else {
- long alarmTimeout = MINIMUM_PNO_GAP_MS - timeDifference;
- Log.d(TAG, "Start PNO timer with delay " + alarmTimeout);
- mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
- mClock.getElapsedSinceBootMillis() + alarmTimeout,
- PNO_DEBOUNCER_ALARM_TAG,
- mAlarmListener, mEventHandler);
- mWaitForTimer = true;
- }
- }
- return isSuccess;
- }
-
- /**
- * Start PNO scan
- */
- public boolean startPnoScan(WifiNative.PnoSettings pnoSettings, Listener listener) {
- if (DBG) Log.d(TAG, "Starting PNO scan");
- mListener = listener;
- mPnoSettings = pnoSettings;
- if (!setPnoState(true)) {
- mListener = null;
- return false;
- }
- return true;
- }
-
- /**
- * Stop PNO scan
- */
- public void stopPnoScan() {
- if (DBG) Log.d(TAG, "Stopping PNO scan");
- setPnoState(false);
- mListener = null;
- }
+ public long startTime;
+ public WifiNative.PnoNetwork[] pnoNetworkList;
+ public WifiNative.PnoEventHandler pnoScanEventHandler;
- /**
- * Force stop PNO scanning. This method will bypass the debounce logic and stop PNO
- * scan immediately.
- */
- public void forceStopPnoScan() {
- if (DBG) Log.d(TAG, "Force stopping Pno scan");
- // Cancel the debounce timer and stop PNO scan.
- if (mWaitForTimer) {
- mAlarmManager.cancel(mAlarmListener);
- mWaitForTimer = false;
- }
- stopPnoScanInternal();
- }
}
+
}
diff --git a/com/android/server/wifi/util/WifiPermissionsUtil.java b/com/android/server/wifi/util/WifiPermissionsUtil.java
index d3c072fb..4ae7d132 100644
--- a/com/android/server/wifi/util/WifiPermissionsUtil.java
+++ b/com/android/server/wifi/util/WifiPermissionsUtil.java
@@ -22,7 +22,6 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.net.NetworkScoreManager;
-import android.os.Binder;
import android.os.RemoteException;
import android.os.UserManager;
import android.provider.Settings;
@@ -202,24 +201,19 @@ public class WifiPermissionsUtil {
* current user.
*/
private boolean isCurrentProfile(int uid) {
- final long token = Binder.clearCallingIdentity();
- try {
- int currentUser = mWifiPermissionsWrapper.getCurrentUser();
- int callingUserId = mWifiPermissionsWrapper.getCallingUserId(uid);
- if (callingUserId == currentUser) {
- return true;
- } else {
- List<UserInfo> userProfiles = mUserManager.getProfiles(currentUser);
- for (UserInfo user : userProfiles) {
- if (user.id == callingUserId) {
- return true;
- }
+ int currentUser = mWifiPermissionsWrapper.getCurrentUser();
+ int callingUserId = mWifiPermissionsWrapper.getCallingUserId(uid);
+ if (callingUserId == currentUser) {
+ return true;
+ } else {
+ List<UserInfo> userProfiles = mUserManager.getProfiles(currentUser);
+ for (UserInfo user : userProfiles) {
+ if (user.id == callingUserId) {
+ return true;
}
}
- return false;
- } finally {
- Binder.restoreCallingIdentity(token);
}
+ return false;
}
/**
diff --git a/com/android/server/wifi/util/WifiPermissionsWrapper.java b/com/android/server/wifi/util/WifiPermissionsWrapper.java
index 6fde01ee..84aacdff 100644
--- a/com/android/server/wifi/util/WifiPermissionsWrapper.java
+++ b/com/android/server/wifi/util/WifiPermissionsWrapper.java
@@ -108,4 +108,16 @@ public class WifiPermissionsWrapper {
return AppGlobals.getPackageManager().checkUidPermission(
Manifest.permission.CHANGE_WIFI_STATE, uid);
}
+
+ /**
+ * Determines if the caller has local mac address permission.
+ *
+ * @param uid to check the permission for
+ * @return int representation of success or denied
+ * @throws RemoteException
+ */
+ public int getLocalMacAddressPermission(int uid) throws RemoteException {
+ return AppGlobals.getPackageManager().checkUidPermission(
+ Manifest.permission.LOCAL_MAC_ADDRESS, uid);
+ }
}
diff --git a/com/android/server/wm/AccessibilityController.java b/com/android/server/wm/AccessibilityController.java
index 95b139ad..163b1600 100644
--- a/com/android/server/wm/AccessibilityController.java
+++ b/com/android/server/wm/AccessibilityController.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
@@ -234,16 +235,7 @@ final class AccessibilityController {
private static void populateTransformationMatrixLocked(WindowState windowState,
Matrix outMatrix) {
- sTempFloats[Matrix.MSCALE_X] = windowState.mWinAnimator.mDsDx;
- sTempFloats[Matrix.MSKEW_Y] = windowState.mWinAnimator.mDtDx;
- sTempFloats[Matrix.MSKEW_X] = windowState.mWinAnimator.mDtDy;
- sTempFloats[Matrix.MSCALE_Y] = windowState.mWinAnimator.mDsDy;
- sTempFloats[Matrix.MTRANS_X] = windowState.mShownPosition.x;
- sTempFloats[Matrix.MTRANS_Y] = windowState.mShownPosition.y;
- sTempFloats[Matrix.MPERSP_0] = 0;
- sTempFloats[Matrix.MPERSP_1] = 0;
- sTempFloats[Matrix.MPERSP_2] = 1;
- outMatrix.setValues(sTempFloats);
+ windowState.getTransformationMatrix(sTempFloats, outMatrix);
}
/**
@@ -1077,8 +1069,11 @@ final class AccessibilityController {
continue;
}
- // If the window is not touchable - ignore.
- if ((flags & WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) != 0) {
+ // Ignore non-touchable windows, except the split-screen divider, which is
+ // occasionally non-touchable but still useful for identifying split-screen
+ // mode.
+ if (((flags & WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) != 0)
+ && (windowState.mAttrs.type != TYPE_DOCK_DIVIDER)) {
continue;
}
diff --git a/com/android/server/wm/AnimationAdapter.java b/com/android/server/wm/AnimationAdapter.java
new file mode 100644
index 00000000..ed4543ef
--- /dev/null
+++ b/com/android/server/wm/AnimationAdapter.java
@@ -0,0 +1,80 @@
+/*
+ * 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 com.android.server.wm;
+
+import android.annotation.ColorInt;
+import android.graphics.Point;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.animation.Animation;
+
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+/**
+ * Interface that describes an animation and bridges the animation start to the component
+ * responsible for running the animation.
+ */
+interface AnimationAdapter {
+
+ long STATUS_BAR_TRANSITION_DURATION = 120L;
+
+ /**
+ * @return Whether we should detach the wallpaper during the animation.
+ * @see Animation#setDetachWallpaper
+ */
+ boolean getDetachWallpaper();
+
+ /**
+ * @return The background color behind the animation.
+ */
+ @ColorInt int getBackgroundColor();
+
+ /**
+ * Requests to start the animation.
+ *
+ * @param animationLeash The surface to run the animation on. See {@link SurfaceAnimator} for an
+ * overview of the mechanism. This surface needs to be released by the
+ * component running the animation after {@code finishCallback} has been
+ * invoked, or after the animation was cancelled.
+ * @param t The Transaction to apply the initial frame of the animation.
+ * @param finishCallback The callback to be invoked when the animation has finished.
+ */
+ void startAnimation(SurfaceControl animationLeash, Transaction t,
+ OnAnimationFinishedCallback finishCallback);
+
+ /**
+ * Called when the animation that was started with {@link #startAnimation} was cancelled by the
+ * window manager.
+ *
+ * @param animationLeash The leash passed to {@link #startAnimation}.
+ */
+ void onAnimationCancelled(SurfaceControl animationLeash);
+
+ /**
+ * @return The approximate duration of the animation, in milliseconds.
+ */
+ long getDurationHint();
+
+ /**
+ * If this animation is run as an app opening animation, this calculates the start time for all
+ * status bar transitions that happen as part of the app opening animation, which will be
+ * forwarded to SystemUI.
+ *
+ * @return the desired start time of the status bar transition, in uptime millis
+ */
+ long getStatusBarTransitionsStartTime();
+}
diff --git a/com/android/server/wm/AppTransition.java b/com/android/server/wm/AppTransition.java
index c2cbced2..2ac75834 100644
--- a/com/android/server/wm/AppTransition.java
+++ b/com/android/server/wm/AppTransition.java
@@ -38,23 +38,22 @@ import static com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpe
import static com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation;
import static com.android.internal.R.styleable.WindowAnimation_wallpaperOpenEnterAnimation;
import static com.android.internal.R.styleable.WindowAnimation_wallpaperOpenExitAnimation;
-import static com.android.server.wm.AppWindowAnimator.PROLONG_ANIMATION_AT_START;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerInternal.AppTransitionListener;
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_AFTER_ANIM;
import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_BEFORE_ANIM;
import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_NONE;
-import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_AFTER_ANIM;
import static com.android.server.wm.proto.AppTransitionProto.APP_TRANSITION_STATE;
import static com.android.server.wm.proto.AppTransitionProto.LAST_USED_APP_TRANSITION;
import android.annotation.Nullable;
import android.app.ActivityManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
-import android.graphics.Bitmap;
import android.graphics.GraphicBuffer;
import android.graphics.Path;
import android.graphics.Rect;
@@ -63,7 +62,9 @@ import android.os.Debug;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
@@ -199,6 +200,13 @@ public class AppTransition implements Dump {
private static final int NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN = 6;
private static final int NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE = 7;
private static final int NEXT_TRANSIT_TYPE_CLIP_REVEAL = 8;
+
+ /**
+ * Refers to the transition to activity started by using {@link
+ * android.content.pm.crossprofile.CrossProfileApps#startMainActivity(ComponentName, UserHandle)
+ * }.
+ */
+ private static final int NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS = 9;
private int mNextAppTransitionType = NEXT_TRANSIT_TYPE_NONE;
// These are the possible states for the enter/exit activities during a thumbnail transition
@@ -407,17 +415,23 @@ public class AppTransition implements Dump {
* @return bit-map of WindowManagerPolicy#FINISH_LAYOUT_REDO_* to indicate whether another
* layout pass needs to be done
*/
- int goodToGo(int transit, AppWindowAnimator topOpeningAppAnimator,
- AppWindowAnimator topClosingAppAnimator, ArraySet<AppWindowToken> openingApps,
+ int goodToGo(int transit, AppWindowToken topOpeningApp,
+ AppWindowToken topClosingApp, ArraySet<AppWindowToken> openingApps,
ArraySet<AppWindowToken> closingApps) {
mNextAppTransition = TRANSIT_UNSET;
mNextAppTransitionFlags = 0;
setAppTransitionState(APP_STATE_RUNNING);
+ final AnimationAdapter topOpeningAnim = topOpeningApp != null
+ ? topOpeningApp.getAnimation()
+ : null;
int redoLayout = notifyAppTransitionStartingLocked(transit,
- topOpeningAppAnimator != null ? topOpeningAppAnimator.mAppToken.token : null,
- topClosingAppAnimator != null ? topClosingAppAnimator.mAppToken.token : null,
- topOpeningAppAnimator != null ? topOpeningAppAnimator.animation : null,
- topClosingAppAnimator != null ? topClosingAppAnimator.animation : null);
+ topOpeningApp != null ? topOpeningApp.token : null,
+ topClosingApp != null ? topClosingApp.token : null,
+ topOpeningAnim != null ? topOpeningAnim.getDurationHint() : 0,
+ topOpeningAnim != null
+ ? topOpeningAnim.getStatusBarTransitionsStartTime()
+ : SystemClock.uptimeMillis(),
+ AnimationAdapter.STATUS_BAR_TRANSITION_DURATION);
mService.getDefaultDisplayContentLocked().getDockedDividerController()
.notifyAppTransitionStarting(openingApps, transit);
@@ -425,8 +439,8 @@ public class AppTransition implements Dump {
// ended it already then we don't need to wait.
if (transit == TRANSIT_DOCK_TASK_FROM_RECENTS && !mProlongedAnimationsEnded) {
for (int i = openingApps.size() - 1; i >= 0; i--) {
- final AppWindowAnimator appAnimator = openingApps.valueAt(i).mAppAnimator;
- appAnimator.startProlongAnimation(PROLONG_ANIMATION_AT_START);
+ final AppWindowToken app = openingApps.valueAt(i);
+ app.startDelayingAnimationStart();
}
}
return redoLayout;
@@ -496,11 +510,12 @@ public class AppTransition implements Dump {
}
private int notifyAppTransitionStartingLocked(int transit, IBinder openToken,
- IBinder closeToken, Animation openAnimation, Animation closeAnimation) {
+ IBinder closeToken, long duration, long statusBarAnimationStartTime,
+ long statusBarAnimationDuration) {
int redoLayout = 0;
for (int i = 0; i < mListeners.size(); i++) {
redoLayout |= mListeners.get(i).onAppTransitionStartingLocked(transit, openToken,
- closeToken, openAnimation, closeAnimation);
+ closeToken, duration, statusBarAnimationStartTime, statusBarAnimationDuration);
}
return redoLayout;
}
@@ -1605,6 +1620,17 @@ public class AppTransition implements Dump {
+ " transit=" + appTransitionToString(transit) + " isEntrance=" + enter
+ " Callers=" + Debug.getCallers(3));
}
+ } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS
+ && (transit == TRANSIT_ACTIVITY_OPEN
+ || transit == TRANSIT_TASK_OPEN
+ || transit == TRANSIT_TASK_TO_FRONT)) {
+ a = loadAnimationRes("android", enter
+ ? com.android.internal.R.anim.activity_open_enter
+ : com.android.internal.R.anim.activity_open_exit);
+ Slog.v(TAG,
+ "applyAnimation NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS:"
+ + " anim=" + a + " transit=" + appTransitionToString(transit)
+ + " isEntrance=" + enter + " Callers=" + Debug.getCallers(3));
} else {
int animAttr = 0;
switch (transit) {
@@ -1833,6 +1859,17 @@ public class AppTransition implements Dump {
}
/**
+ * @see {@link #NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS}
+ */
+ void overridePendingAppTransitionStartCrossProfileApps() {
+ if (isTransitionSet()) {
+ clear();
+ mNextAppTransitionType = NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS;
+ postAnimationCallback();
+ }
+ }
+
+ /**
* If a future is set for the app transition specs, fetch it in another thread.
*/
private void fetchAppTransitionSpecsFromFuture() {
@@ -1855,9 +1892,6 @@ public class AppTransition implements Dump {
mNextAppTransitionFutureCallback, null /* finishedCallback */,
mNextAppTransitionScaleUp);
mNextAppTransitionFutureCallback = null;
- if (specs != null) {
- mService.prolongAnimationsFromSpecs(specs, mNextAppTransitionScaleUp);
- }
}
mService.requestTraversal();
});
diff --git a/com/android/server/wm/AppWindowAnimator.java b/com/android/server/wm/AppWindowAnimator.java
deleted file mode 100644
index 5c1d5b25..00000000
--- a/com/android/server/wm/AppWindowAnimator.java
+++ /dev/null
@@ -1,495 +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 com.android.server.wm;
-
-import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
-import static com.android.server.wm.AppTransition.TRANSIT_UNSET;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYERS;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_VISIBILITY;
-import static com.android.server.wm.WindowManagerDebugConfig.SHOW_TRANSACTIONS;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
-import static com.android.server.wm.WindowManagerService.TYPE_LAYER_OFFSET;
-import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_BEFORE_ANIM;
-
-import android.graphics.Matrix;
-import android.util.Slog;
-import android.util.TimeUtils;
-import android.view.Choreographer;
-import android.view.Display;
-import android.view.SurfaceControl;
-import android.view.animation.Animation;
-import android.view.animation.Transformation;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-
-public class AppWindowAnimator {
- static final String TAG = TAG_WITH_CLASS_NAME ? "AppWindowAnimator" : TAG_WM;
-
- private static final int PROLONG_ANIMATION_DISABLED = 0;
- static final int PROLONG_ANIMATION_AT_END = 1;
- static final int PROLONG_ANIMATION_AT_START = 2;
-
- final AppWindowToken mAppToken;
- final WindowManagerService mService;
- final WindowAnimator mAnimator;
-
- boolean animating;
- boolean wasAnimating;
- Animation animation;
- boolean hasTransformation;
- final Transformation transformation = new Transformation();
-
- // Have we been asked to have this token keep the screen frozen?
- // Protect with mAnimator.
- boolean freezingScreen;
-
- /**
- * How long we last kept the screen frozen.
- */
- int lastFreezeDuration;
-
- // Offset to the window of all layers in the token, for use by
- // AppWindowToken animations.
- int animLayerAdjustment;
-
- // Propagated from AppWindowToken.allDrawn, to determine when
- // the state changes.
- boolean allDrawn;
-
- // Special surface for thumbnail animation. If deferThumbnailDestruction is enabled, then we
- // will make sure that the thumbnail is destroyed after the other surface is completed. This
- // requires that the duration of the two animations are the same.
- SurfaceControl thumbnail;
- int thumbnailTransactionSeq;
- private int mThumbnailLayer;
-
- Animation thumbnailAnimation;
- final Transformation thumbnailTransformation = new Transformation();
- // This flag indicates that the destruction of the thumbnail surface is synchronized with
- // another animation, so defer the destruction of this thumbnail surface for a single frame
- // after the secondary animation completes.
- boolean deferThumbnailDestruction;
- // This flag is set if the animator has deferThumbnailDestruction set and has reached the final
- // frame of animation. It will extend the animation by one frame and then clean up afterwards.
- boolean deferFinalFrameCleanup;
- // If true when the animation hits the last frame, it will keep running on that last frame.
- // This is used to synchronize animation with Recents and we wait for Recents to tell us to
- // finish or for a new animation be set as fail-safe mechanism.
- private int mProlongAnimation;
- // Whether the prolong animation can be removed when animation is set. The purpose of this is
- // that if recents doesn't tell us to remove the prolonged animation, we will get rid of it
- // when new animation is set.
- private boolean mClearProlongedAnimation;
- private int mTransit;
- private int mTransitFlags;
-
- /** WindowStateAnimator from mAppAnimator.allAppWindows as of last performLayout */
- ArrayList<WindowStateAnimator> mAllAppWinAnimators = new ArrayList<>();
-
- /** True if the current animation was transferred from another AppWindowAnimator.
- * See {@link #transferCurrentAnimation}*/
- boolean usingTransferredAnimation = false;
-
- private boolean mSkipFirstFrame = false;
- private int mStackClip = STACK_CLIP_BEFORE_ANIM;
-
- static final Animation sDummyAnimation = new DummyAnimation();
-
- public AppWindowAnimator(final AppWindowToken atoken, WindowManagerService service) {
- mAppToken = atoken;
- mService = service;
- mAnimator = mService.mAnimator;
- }
-
- public void setAnimation(Animation anim, int width, int height, int parentWidth,
- int parentHeight, boolean skipFirstFrame, int stackClip, int transit,
- int transitFlags) {
- if (WindowManagerService.localLOGV) Slog.v(TAG, "Setting animation in " + mAppToken
- + ": " + anim + " wxh=" + width + "x" + height
- + " hasContentToDisplay=" + mAppToken.hasContentToDisplay());
- animation = anim;
- animating = false;
- if (!anim.isInitialized()) {
- anim.initialize(width, height, parentWidth, parentHeight);
- }
- anim.restrictDuration(WindowManagerService.MAX_ANIMATION_DURATION);
- anim.scaleCurrentDuration(mService.getTransitionAnimationScaleLocked());
- int zorder = anim.getZAdjustment();
- int adj = 0;
- if (zorder == Animation.ZORDER_TOP) {
- adj = TYPE_LAYER_OFFSET;
- } else if (zorder == Animation.ZORDER_BOTTOM) {
- adj = -TYPE_LAYER_OFFSET;
- }
-
- if (animLayerAdjustment != adj) {
- animLayerAdjustment = adj;
- updateLayers();
- }
- // Start out animation gone if window is gone, or visible if window is visible.
- transformation.clear();
- transformation.setAlpha(mAppToken.isVisible() ? 1 : 0);
- hasTransformation = true;
- mStackClip = stackClip;
-
- mSkipFirstFrame = skipFirstFrame;
- mTransit = transit;
- mTransitFlags = transitFlags;
-
- if (!mAppToken.fillsParent()) {
- anim.setBackgroundColor(0);
- }
- if (mClearProlongedAnimation) {
- mProlongAnimation = PROLONG_ANIMATION_DISABLED;
- } else {
- mClearProlongedAnimation = true;
- }
- }
-
- public void setDummyAnimation() {
- if (WindowManagerService.localLOGV) Slog.v(TAG, "Setting dummy animation in " + mAppToken
- + " hasContentToDisplay=" + mAppToken.hasContentToDisplay());
- animation = sDummyAnimation;
- hasTransformation = true;
- transformation.clear();
- transformation.setAlpha(mAppToken.isVisible() ? 1 : 0);
- }
-
- void setNullAnimation() {
- animation = null;
- usingTransferredAnimation = false;
- }
-
- public void clearAnimation() {
- if (animation != null) {
- animating = true;
- }
- clearThumbnail();
- setNullAnimation();
- if (mAppToken.deferClearAllDrawn) {
- mAppToken.clearAllDrawn();
- }
- mStackClip = STACK_CLIP_BEFORE_ANIM;
- mTransit = TRANSIT_UNSET;
- mTransitFlags = 0;
- }
-
- public boolean isAnimating() {
- return animation != null || mAppToken.inPendingTransaction;
- }
-
- /**
- * @return whether an animation is about to start, i.e. the animation is set already but we
- * haven't processed the first frame yet.
- */
- boolean isAnimationStarting() {
- return animation != null && !animating;
- }
-
- public int getTransit() {
- return mTransit;
- }
-
- int getTransitFlags() {
- return mTransitFlags;
- }
-
- public void clearThumbnail() {
- if (thumbnail != null) {
- thumbnail.hide();
- mService.mWindowPlacerLocked.destroyAfterTransaction(thumbnail);
- thumbnail = null;
- }
- deferThumbnailDestruction = false;
- }
-
- int getStackClip() {
- return mStackClip;
- }
-
- void transferCurrentAnimation(
- AppWindowAnimator toAppAnimator, WindowStateAnimator transferWinAnimator) {
-
- if (animation != null) {
- toAppAnimator.animation = animation;
- toAppAnimator.animating = animating;
- toAppAnimator.animLayerAdjustment = animLayerAdjustment;
- setNullAnimation();
- animLayerAdjustment = 0;
- toAppAnimator.updateLayers();
- updateLayers();
- toAppAnimator.usingTransferredAnimation = true;
- toAppAnimator.mTransit = mTransit;
- }
- if (transferWinAnimator != null) {
- mAllAppWinAnimators.remove(transferWinAnimator);
- toAppAnimator.mAllAppWinAnimators.add(transferWinAnimator);
- toAppAnimator.hasTransformation = transferWinAnimator.mAppAnimator.hasTransformation;
- if (toAppAnimator.hasTransformation) {
- toAppAnimator.transformation.set(transferWinAnimator.mAppAnimator.transformation);
- } else {
- toAppAnimator.transformation.clear();
- }
- transferWinAnimator.mAppAnimator = toAppAnimator;
- }
- }
-
- private void updateLayers() {
- mAppToken.getDisplayContent().assignWindowLayers(false /* relayoutNeeded */);
- }
-
- private void stepThumbnailAnimation(long currentTime) {
- thumbnailTransformation.clear();
- final long animationFrameTime = getAnimationFrameTime(thumbnailAnimation, currentTime);
- thumbnailAnimation.getTransformation(animationFrameTime, thumbnailTransformation);
-
- ScreenRotationAnimation screenRotationAnimation =
- mAnimator.getScreenRotationAnimationLocked(Display.DEFAULT_DISPLAY);
- final boolean screenAnimation = screenRotationAnimation != null
- && screenRotationAnimation.isAnimating();
- if (screenAnimation) {
- thumbnailTransformation.postCompose(screenRotationAnimation.getEnterTransformation());
- }
- // cache often used attributes locally
- final float tmpFloats[] = mService.mTmpFloats;
- thumbnailTransformation.getMatrix().getValues(tmpFloats);
- if (SHOW_TRANSACTIONS) WindowManagerService.logSurface(thumbnail,
- "thumbnail", "POS " + tmpFloats[Matrix.MTRANS_X]
- + ", " + tmpFloats[Matrix.MTRANS_Y]);
- thumbnail.setPosition(tmpFloats[Matrix.MTRANS_X], tmpFloats[Matrix.MTRANS_Y]);
- if (SHOW_TRANSACTIONS) WindowManagerService.logSurface(thumbnail,
- "thumbnail", "alpha=" + thumbnailTransformation.getAlpha()
- + " layer=" + mThumbnailLayer
- + " matrix=[" + tmpFloats[Matrix.MSCALE_X]
- + "," + tmpFloats[Matrix.MSKEW_Y]
- + "][" + tmpFloats[Matrix.MSKEW_X]
- + "," + tmpFloats[Matrix.MSCALE_Y] + "]");
- thumbnail.setAlpha(thumbnailTransformation.getAlpha());
- thumbnail.setMatrix(tmpFloats[Matrix.MSCALE_X], tmpFloats[Matrix.MSKEW_Y],
- tmpFloats[Matrix.MSKEW_X], tmpFloats[Matrix.MSCALE_Y]);
- thumbnail.setWindowCrop(thumbnailTransformation.getClipRect());
- }
-
- /**
- * Sometimes we need to synchronize the first frame of animation with some external event, e.g.
- * Recents hiding some of its content. To achieve this, we prolong the start of the animaiton
- * and keep producing the first frame of the animation.
- */
- private long getAnimationFrameTime(Animation animation, long currentTime) {
- if (mProlongAnimation == PROLONG_ANIMATION_AT_START) {
- animation.setStartTime(currentTime);
- return currentTime + 1;
- }
- return currentTime;
- }
-
- private boolean stepAnimation(long currentTime) {
- if (animation == null) {
- return false;
- }
- transformation.clear();
- final long animationFrameTime = getAnimationFrameTime(animation, currentTime);
- boolean hasMoreFrames = animation.getTransformation(animationFrameTime, transformation);
- if (!hasMoreFrames) {
- if (deferThumbnailDestruction && !deferFinalFrameCleanup) {
- // We are deferring the thumbnail destruction, so extend the animation for one more
- // (dummy) frame before we clean up
- deferFinalFrameCleanup = true;
- hasMoreFrames = true;
- } else {
- if (false && DEBUG_ANIM) Slog.v(TAG,
- "Stepped animation in " + mAppToken + ": more=" + hasMoreFrames +
- ", xform=" + transformation + ", mProlongAnimation=" + mProlongAnimation);
- deferFinalFrameCleanup = false;
- if (mProlongAnimation == PROLONG_ANIMATION_AT_END) {
- hasMoreFrames = true;
- } else {
- setNullAnimation();
- clearThumbnail();
- if (DEBUG_ANIM) Slog.v(TAG, "Finished animation in " + mAppToken + " @ "
- + currentTime);
- }
- }
- }
- hasTransformation = hasMoreFrames;
- return hasMoreFrames;
- }
-
- private long getStartTimeCorrection() {
- if (mSkipFirstFrame) {
-
- // If the transition is an animation in which the first frame doesn't change the screen
- // contents at all, we can just skip it and start at the second frame. So we shift the
- // start time of the animation forward by minus the frame duration.
- return -Choreographer.getInstance().getFrameIntervalNanos() / TimeUtils.NANOS_PER_MS;
- } else {
- return 0;
- }
- }
-
- // This must be called while inside a transaction.
- boolean stepAnimationLocked(long currentTime) {
- if (mAppToken.okToAnimate()) {
- // We will run animations as long as the display isn't frozen.
-
- if (animation == sDummyAnimation) {
- // This guy is going to animate, but not yet. For now count
- // it as not animating for purposes of scheduling transactions;
- // when it is really time to animate, this will be set to
- // a real animation and the next call will execute normally.
- return false;
- }
-
- if ((mAppToken.allDrawn || animating || mAppToken.startingDisplayed)
- && animation != null) {
- if (!animating) {
- if (DEBUG_ANIM) Slog.v(TAG,
- "Starting animation in " + mAppToken +
- " @ " + currentTime + " scale="
- + mService.getTransitionAnimationScaleLocked()
- + " allDrawn=" + mAppToken.allDrawn + " animating=" + animating);
- long correction = getStartTimeCorrection();
- animation.setStartTime(currentTime + correction);
- animating = true;
- if (thumbnail != null) {
- thumbnail.show();
- thumbnailAnimation.setStartTime(currentTime + correction);
- }
- mSkipFirstFrame = false;
- }
- if (stepAnimation(currentTime)) {
- // animation isn't over, step any thumbnail and that's
- // it for now.
- if (thumbnail != null) {
- stepThumbnailAnimation(currentTime);
- }
- return true;
- }
- }
- } else if (animation != null) {
- // If the display is frozen, and there is a pending animation,
- // clear it and make sure we run the cleanup code.
- animating = true;
- animation = null;
- }
-
- hasTransformation = false;
-
- if (!animating && animation == null) {
- return false;
- }
-
- mAppToken.setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM, "AppWindowToken");
-
- clearAnimation();
- animating = false;
- if (animLayerAdjustment != 0) {
- animLayerAdjustment = 0;
- updateLayers();
- }
- if (mService.mInputMethodTarget != null
- && mService.mInputMethodTarget.mAppToken == mAppToken) {
- mAppToken.getDisplayContent().computeImeTarget(true /* updateImeTarget */);
- }
-
- if (DEBUG_ANIM) Slog.v(TAG, "Animation done in " + mAppToken
- + ": reportedVisible=" + mAppToken.reportedVisible
- + " okToDisplay=" + mAppToken.okToDisplay()
- + " okToAnimate=" + mAppToken.okToAnimate()
- + " startingDisplayed=" + mAppToken.startingDisplayed);
-
- transformation.clear();
-
- final int numAllAppWinAnimators = mAllAppWinAnimators.size();
- for (int i = 0; i < numAllAppWinAnimators; i++) {
- mAllAppWinAnimators.get(i).mWin.onExitAnimationDone();
- }
- mService.mAppTransition.notifyAppTransitionFinishedLocked(mAppToken.token);
- return false;
- }
-
- // This must be called while inside a transaction.
- boolean showAllWindowsLocked() {
- boolean isAnimating = false;
- final int NW = mAllAppWinAnimators.size();
- for (int i=0; i<NW; i++) {
- WindowStateAnimator winAnimator = mAllAppWinAnimators.get(i);
- if (DEBUG_VISIBILITY) Slog.v(TAG, "performing show on: " + winAnimator);
- winAnimator.mWin.performShowLocked();
- isAnimating |= winAnimator.isAnimationSet();
- }
- return isAnimating;
- }
-
- void dump(PrintWriter pw, String prefix) {
- pw.print(prefix); pw.print("mAppToken="); pw.println(mAppToken);
- pw.print(prefix); pw.print("mAnimator="); pw.println(mAnimator);
- pw.print(prefix); pw.print("freezingScreen="); pw.print(freezingScreen);
- pw.print(" allDrawn="); pw.print(allDrawn);
- pw.print(" animLayerAdjustment="); pw.println(animLayerAdjustment);
- if (lastFreezeDuration != 0) {
- pw.print(prefix); pw.print("lastFreezeDuration=");
- TimeUtils.formatDuration(lastFreezeDuration, pw); pw.println();
- }
- if (animating || animation != null) {
- pw.print(prefix); pw.print("animating="); pw.println(animating);
- pw.print(prefix); pw.print("animation="); pw.println(animation);
- pw.print(prefix); pw.print("mTransit="); pw.println(mTransit);
- pw.print(prefix); pw.print("mTransitFlags="); pw.println(mTransitFlags);
- }
- if (hasTransformation) {
- pw.print(prefix); pw.print("XForm: ");
- transformation.printShortString(pw);
- pw.println();
- }
- if (thumbnail != null) {
- pw.print(prefix); pw.print("thumbnail="); pw.print(thumbnail);
- pw.print(" layer="); pw.println(mThumbnailLayer);
- pw.print(prefix); pw.print("thumbnailAnimation="); pw.println(thumbnailAnimation);
- pw.print(prefix); pw.print("thumbnailTransformation=");
- pw.println(thumbnailTransformation.toShortString());
- }
- for (int i=0; i<mAllAppWinAnimators.size(); i++) {
- WindowStateAnimator wanim = mAllAppWinAnimators.get(i);
- pw.print(prefix); pw.print("App Win Anim #"); pw.print(i);
- pw.print(": "); pw.println(wanim);
- }
- }
-
- void startProlongAnimation(int prolongType) {
- mProlongAnimation = prolongType;
- mClearProlongedAnimation = false;
- }
-
- void endProlongedAnimation() {
- mProlongAnimation = PROLONG_ANIMATION_DISABLED;
- }
-
- // This is an animation that does nothing: it just immediately finishes
- // itself every time it is called. It is used as a stub animation in cases
- // where we want to synchronize multiple things that may be animating.
- static final class DummyAnimation extends Animation {
- @Override
- public boolean getTransformation(long currentTime, Transformation outTransformation) {
- return false;
- }
- }
-
-}
diff --git a/com/android/server/wm/AppWindowContainerController.java b/com/android/server/wm/AppWindowContainerController.java
index 00a0d3d1..ae9f28bc 100644
--- a/com/android/server/wm/AppWindowContainerController.java
+++ b/com/android/server/wm/AppWindowContainerController.java
@@ -17,7 +17,6 @@
package com.android.server.wm;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
import static com.android.server.wm.AppTransition.TRANSIT_DOCK_TASK_FROM_RECENTS;
@@ -25,7 +24,6 @@ import static com.android.server.wm.AppTransition.TRANSIT_UNSET;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ORIENTATION;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WINDOW;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TOKEN_MOVEMENT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_VISIBILITY;
@@ -34,16 +32,13 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.app.ActivityManager.TaskSnapshot;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
-import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Debug;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
-import android.os.Trace;
import android.util.Slog;
-import android.view.DisplayInfo;
import android.view.IApplicationToken;
import com.android.internal.annotations.VisibleForTesting;
@@ -339,7 +334,7 @@ public class AppWindowContainerController
if (DEBUG_APP_TRANSITIONS || DEBUG_ORIENTATION) Slog.v(TAG_WM, "setAppVisibility("
+ mToken + ", visible=" + visible + "): " + mService.mAppTransition
- + " hidden=" + wtoken.hidden + " hiddenRequested="
+ + " hidden=" + wtoken.isHidden() + " hiddenRequested="
+ wtoken.hiddenRequested + " Callers=" + Debug.getCallers(6));
mService.mOpeningApps.remove(wtoken);
@@ -364,11 +359,11 @@ public class AppWindowContainerController
wtoken.startingMoved = false;
// If the token is currently hidden (should be the common case), or has been
// stopped, then we need to set up to wait for its windows to be ready.
- if (wtoken.hidden || wtoken.mAppStopped) {
+ if (wtoken.isHidden() || wtoken.mAppStopped) {
wtoken.clearAllDrawn();
// If the app was already visible, don't reset the waitingToShow state.
- if (wtoken.hidden) {
+ if (wtoken.isHidden()) {
wtoken.waitingToShow = true;
}
@@ -389,21 +384,6 @@ public class AppWindowContainerController
// If we are preparing an app transition, then delay changing
// the visibility of this token until we execute that transition.
if (wtoken.okToAnimate() && mService.mAppTransition.isTransitionSet()) {
- // A dummy animation is a placeholder animation which informs others that an
- // animation is going on (in this case an application transition). If the animation
- // was transferred from another application/animator, no dummy animator should be
- // created since an animation is already in progress.
- if (wtoken.mAppAnimator.usingTransferredAnimation
- && wtoken.mAppAnimator.animation == null) {
- Slog.wtf(TAG_WM, "Will NOT set dummy animation on: " + wtoken
- + ", using null transferred animation!");
- }
- if (!wtoken.mAppAnimator.usingTransferredAnimation &&
- (!wtoken.startingDisplayed || mService.mSkipAppTransitionAnimation)) {
- if (DEBUG_APP_TRANSITIONS) Slog.v(
- TAG_WM, "Setting dummy animation on: " + wtoken);
- wtoken.mAppAnimator.setDummyAnimation();
- }
wtoken.inPendingTransaction = true;
if (visible) {
mService.mOpeningApps.add(wtoken);
@@ -423,7 +403,7 @@ public class AppWindowContainerController
if (DEBUG_APP_TRANSITIONS) Slog.d(TAG_WM, "TRANSIT_TASK_OPEN_BEHIND, "
+ " adding " + focusedToken + " to mOpeningApps");
// Force animation to be loaded.
- focusedToken.hidden = true;
+ focusedToken.setHidden(true);
mService.mOpeningApps.add(focusedToken);
}
}
@@ -710,7 +690,7 @@ public class AppWindowContainerController
return;
}
if (DEBUG_ORIENTATION) Slog.v(TAG_WM, "Clear freezing of " + mToken + ": hidden="
- + mContainer.hidden + " freezing=" + mContainer.mAppAnimator.freezingScreen);
+ + mContainer.isHidden() + " freezing=" + mContainer.isFreezingScreen());
mContainer.stopFreezingScreen(true, force);
}
}
diff --git a/com/android/server/wm/AppWindowThumbnail.java b/com/android/server/wm/AppWindowThumbnail.java
new file mode 100644
index 00000000..b86cd50d
--- /dev/null
+++ b/com/android/server/wm/AppWindowThumbnail.java
@@ -0,0 +1,164 @@
+/*
+ * 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 com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.SHOW_TRANSACTIONS;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+import static com.android.server.wm.WindowManagerService.MAX_ANIMATION_DURATION;
+
+import android.graphics.GraphicBuffer;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.os.Binder;
+import android.util.Slog;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Builder;
+import android.view.SurfaceControl.Transaction;
+import android.view.animation.Animation;
+
+import com.android.server.wm.SurfaceAnimator.Animatable;
+
+/**
+ * Represents a surface that is displayed over an {@link AppWindowToken}
+ */
+class AppWindowThumbnail implements Animatable {
+
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "AppWindowThumbnail" : TAG_WM;
+
+ private final AppWindowToken mAppToken;
+ private final SurfaceControl mSurfaceControl;
+ private final SurfaceAnimator mSurfaceAnimator;
+ private final int mWidth;
+ private final int mHeight;
+
+ AppWindowThumbnail(Transaction t, AppWindowToken appToken, GraphicBuffer thumbnailHeader) {
+ mAppToken = appToken;
+ mSurfaceAnimator = new SurfaceAnimator(this, this::onAnimationFinished, appToken.mService);
+ mWidth = thumbnailHeader.getWidth();
+ mHeight = thumbnailHeader.getHeight();
+
+ // Create a new surface for the thumbnail
+ WindowState window = appToken.findMainWindow();
+
+ // TODO: This should be attached as a child to the app token, once the thumbnail animations
+ // use relative coordinates. Once we start animating task we can also consider attaching
+ // this to the task.
+ mSurfaceControl = appToken.makeSurface()
+ .setName("thumbnail anim: " + appToken.toString())
+ .setSize(mWidth, mHeight)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .setMetadata(appToken.windowType,
+ window != null ? window.mOwnerUid : Binder.getCallingUid())
+ .build();
+
+ if (SHOW_TRANSACTIONS) {
+ Slog.i(TAG, " THUMBNAIL " + mSurfaceControl + ": CREATE");
+ }
+
+ // Transfer the thumbnail to the surface
+ Surface drawSurface = new Surface();
+ drawSurface.copyFrom(mSurfaceControl);
+ drawSurface.attachAndQueueBuffer(thumbnailHeader);
+ drawSurface.release();
+ t.show(mSurfaceControl);
+
+ // We parent the thumbnail to the task, and just place it on top of anything else in the
+ // task.
+ t.setLayer(mSurfaceControl, Integer.MAX_VALUE);
+ }
+
+ void startAnimation(Transaction t, Animation anim) {
+ anim.restrictDuration(MAX_ANIMATION_DURATION);
+ anim.scaleCurrentDuration(mAppToken.mService.getTransitionAnimationScaleLocked());
+ mSurfaceAnimator.startAnimation(t, new LocalAnimationAdapter(
+ new WindowAnimationSpec(anim, null /* position */,
+ mAppToken.mService.mAppTransition.canSkipFirstFrame()),
+ mAppToken.mService.mSurfaceAnimationRunner), false /* hidden */);
+ }
+
+ private void onAnimationFinished() {
+ }
+
+ void setShowing(Transaction pendingTransaction, boolean show) {
+ // TODO: Not needed anymore once thumbnail is attached to the app.
+ if (show) {
+ pendingTransaction.show(mSurfaceControl);
+ } else {
+ pendingTransaction.hide(mSurfaceControl);
+ }
+ }
+
+ void destroy() {
+ mSurfaceAnimator.cancelAnimation();
+ mSurfaceControl.destroy();
+ }
+
+ @Override
+ public Transaction getPendingTransaction() {
+ return mAppToken.getPendingTransaction();
+ }
+
+ @Override
+ public void commitPendingTransaction() {
+ mAppToken.commitPendingTransaction();
+ }
+
+ @Override
+ public void destroyAfterPendingTransaction(SurfaceControl surface) {
+ mAppToken.destroyAfterPendingTransaction(surface);
+ }
+
+ @Override
+ public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
+ t.setLayer(leash, Integer.MAX_VALUE);
+ }
+
+ @Override
+ public void onAnimationLeashDestroyed(Transaction t) {
+
+ // TODO: Once attached to app token, we don't need to hide it immediately if thumbnail
+ // became visible.
+ t.hide(mSurfaceControl);
+ }
+
+ @Override
+ public Builder makeAnimationLeash() {
+ return mAppToken.makeSurface().setParent(mAppToken.getAppAnimationLayer());
+ }
+
+ @Override
+ public SurfaceControl getSurfaceControl() {
+ return mSurfaceControl;
+ }
+
+ @Override
+ public SurfaceControl getParentSurfaceControl() {
+ return mAppToken.getParentSurfaceControl();
+ }
+
+ @Override
+ public int getSurfaceWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getSurfaceHeight() {
+ return mHeight;
+ }
+}
diff --git a/com/android/server/wm/AppWindowToken.java b/com/android/server/wm/AppWindowToken.java
index c39ce982..44d7948b 100644
--- a/com/android/server/wm/AppWindowToken.java
+++ b/com/android/server/wm/AppWindowToken.java
@@ -20,6 +20,7 @@ import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
@@ -51,17 +52,21 @@ import static com.android.server.wm.proto.AppWindowTokenProto.NAME;
import static com.android.server.wm.proto.AppWindowTokenProto.WINDOW_TOKEN;
import android.annotation.CallSuper;
-import android.annotation.NonNull;
import android.app.Activity;
import android.content.res.Configuration;
+import android.graphics.GraphicBuffer;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Debug;
import android.os.IBinder;
import android.os.RemoteException;
-import android.os.SystemClock;
+import android.os.Trace;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
+import android.view.DisplayInfo;
+import android.view.SurfaceControl.Transaction;
+import android.view.animation.Animation;
import android.view.IApplicationToken;
import android.view.SurfaceControl;
import android.view.WindowManager;
@@ -75,6 +80,7 @@ import com.android.server.wm.WindowManagerService.H;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.LinkedList;
class AppTokenList extends ArrayList<AppWindowToken> {
}
@@ -86,11 +92,14 @@ class AppTokenList extends ArrayList<AppWindowToken> {
class AppWindowToken extends WindowToken implements WindowManagerService.AppFreezeListener {
private static final String TAG = TAG_WITH_CLASS_NAME ? "AppWindowToken" : TAG_WM;
+ /**
+ * Value to increment the z-layer when boosting a layer during animations. BOOST in l33tsp34k.
+ */
+ private static final int Z_BOOST_BASE = 800570000;
+
// Non-null only for application tokens.
final IApplicationToken appToken;
- @NonNull final AppWindowAnimator mAppAnimator;
-
final boolean mVoiceInteraction;
/** @see WindowContainer#fillsParent() */
@@ -118,6 +127,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
private int mNumDrawnWindows;
boolean inPendingTransaction;
boolean allDrawn;
+ private boolean mLastAllDrawn;
+
// Set to true when this app creates a surface while in the middle of an animation. In that
// case do not clear allDrawn until the animation completes.
boolean deferClearAllDrawn;
@@ -187,6 +198,32 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
*/
private boolean mCanTurnScreenOn = true;
+ /**
+ * If we are running an animation, this determines the transition type. Must be one of
+ * AppTransition.TRANSIT_* constants.
+ */
+ private int mTransit;
+
+ /**
+ * If we are running an animation, this determines the flags during this animation. Must be a
+ * bitwise combination of AppTransition.TRANSIT_FLAG_* constants.
+ */
+ private int mTransitFlags;
+
+ /** Whether our surface was set to be showing in the last call to {@link #prepareSurfaces} */
+ private boolean mLastSurfaceShowing = true;
+
+ private AppWindowThumbnail mThumbnail;
+
+ /** Have we been asked to have this token keep the screen frozen? */
+ private boolean mFreezingScreen;
+
+ /** Whether this token should be boosted at the top of all app window tokens. */
+ private boolean mNeedsZBoost;
+
+ private final Point mTmpPoint = new Point();
+ private final Rect mTmpRect = new Rect();
+
AppWindowToken(WindowManagerService service, IApplicationToken token, boolean voiceInteraction,
DisplayContent dc, long inputDispatchingTimeoutNanos, boolean fullscreen,
boolean showForAllUsers, int targetSdk, int orientation, int rotationAnimationHint,
@@ -204,7 +241,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
mRotationAnimationHint = rotationAnimationHint;
// Application tokens start out hidden.
- hidden = true;
+ setHidden(true);
hiddenRequested = true;
}
@@ -216,7 +253,6 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
mVoiceInteraction = voiceInteraction;
mFillsParent = fillsParent;
mInputApplicationHandle = new InputApplicationHandle(this);
- mAppAnimator = new AppWindowAnimator(this, service);
}
void onFirstWindowDrawn(WindowState win, WindowStateAnimator winAnimator) {
@@ -231,7 +267,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
// If this initial window is animating, stop it -- we will do an animation to reveal
// it from behind the starting window, so there is no need for it to also be doing its
// own stuff.
- winAnimator.clearAnimation();
+ win.cancelAnimation();
if (getController() != null) {
getController().removeStartingWindow();
}
@@ -260,7 +296,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
boolean nowGone = mReportedVisibilityResults.nowGone;
boolean nowDrawn = numInteresting > 0 && numDrawn >= numInteresting;
- boolean nowVisible = numInteresting > 0 && numVisible >= numInteresting && !hidden;
+ boolean nowVisible = numInteresting > 0 && numVisible >= numInteresting && !isHidden();
if (!nowGone) {
// If the app is not yet gone, then it can only become visible/drawn.
if (!nowDrawn) {
@@ -323,19 +359,16 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
// transition animation
// * or this is an opening app and windows are being replaced.
boolean visibilityChanged = false;
- if (hidden == visible || (hidden && mIsExiting) || (visible && waitingForReplacement())) {
+ if (isHidden() == visible || (isHidden() && mIsExiting) || (visible && waitingForReplacement())) {
final AccessibilityController accessibilityController = mService.mAccessibilityController;
boolean changed = false;
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG_WM,
- "Changing app " + this + " hidden=" + hidden + " performLayout=" + performLayout);
+ "Changing app " + this + " hidden=" + isHidden() + " performLayout=" + performLayout);
boolean runningAppAnimation = false;
- if (mAppAnimator.animation == AppWindowAnimator.sDummyAnimation) {
- mAppAnimator.setNullAnimation();
- }
if (transit != AppTransition.TRANSIT_UNSET) {
- if (mService.applyAnimationLocked(this, lp, transit, visible, isVoiceInteraction)) {
+ if (applyAnimationLocked(lp, transit, visible, isVoiceInteraction)) {
delayed = runningAppAnimation = true;
}
final WindowState window = findMainWindow();
@@ -353,7 +386,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
changed |= win.onAppVisibilityChanged(visible, runningAppAnimation);
}
- hidden = hiddenRequested = !visible;
+ setHidden(!visible);
+ hiddenRequested = !visible;
visibilityChanged = true;
if (!visible) {
stopFreezingScreen(true, true);
@@ -371,7 +405,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG_WM, "setVisibility: " + this
- + ": hidden=" + hidden + " hiddenRequested=" + hiddenRequested);
+ + ": hidden=" + isHidden() + " hiddenRequested=" + hiddenRequested);
if (changed) {
mService.mInputMonitor.setUpdateInputWindowsNeededLw();
@@ -384,12 +418,12 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
}
- if (mAppAnimator.animation != null) {
+ if (isReallyAnimating()) {
delayed = true;
}
for (int i = mChildren.size() - 1; i >= 0 && !delayed; i--) {
- if ((mChildren.get(i)).isWindowAnimationSet()) {
+ if ((mChildren.get(i)).isSelfOrChildAnimating()) {
delayed = true;
}
}
@@ -412,7 +446,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
// no animation but there will still be a transition set.
// We still need to delay hiding the surface such that it
// can be synchronized with showing the next surface in the transition.
- if (hidden && !delayed && !mService.mAppTransition.isTransitionSet()) {
+ if (isHidden() && !delayed && !mService.mAppTransition.isTransitionSet()) {
SurfaceControl.openTransaction();
for (int i = mChildren.size() - 1; i >= 0; i--) {
mChildren.get(i).mWinAnimator.hide("immediately hidden");
@@ -485,7 +519,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
boolean isVisible() {
// If the app token isn't hidden then it is considered visible and there is no need to check
// its children windows to see if they are visible.
- return !hidden;
+ return !isHidden();
}
@Override
@@ -531,7 +565,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG_WM, "Removing app " + this + " delayed=" + delayed
- + " animation=" + mAppAnimator.animation + " animating=" + mAppAnimator.animating);
+ + " animation=" + getAnimation() + " animating=" + isSelfAnimating());
if (DEBUG_ADD_REMOVE || DEBUG_TOKEN_MOVEMENT) Slog.v(TAG_WM, "removeAppToken: "
+ this + " delayed=" + delayed + " Callers=" + Debug.getCallers(4));
@@ -543,7 +577,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
// If this window was animating, then we need to ensure that the app transition notifies
// that animations have completed in WMS.handleAnimatingStoppedAndTransitionLocked(), so
// add to that list now
- if (mAppAnimator.animating) {
+ if (isSelfAnimating()) {
mService.mNoAnimationNotifyOnTransitionFinished.add(token);
}
@@ -559,8 +593,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
} else {
// Make sure there is no animation running on this token, so any windows associated
// with it will be removed as soon as their animations are complete
- mAppAnimator.clearAnimation();
- mAppAnimator.animating = false;
+ cancelAnimation();
if (stack != null) {
stack.mExitingAppTokens.remove(this);
}
@@ -610,8 +643,12 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
*/
private void destroySurfaces(boolean cleanupOnResume) {
boolean destroyedSomething = false;
- for (int i = mChildren.size() - 1; i >= 0; i--) {
- final WindowState win = mChildren.get(i);
+
+ // Copying to a different list as multiple children can be removed.
+ // TODO: Not sure why this is needed.
+ final LinkedList<WindowState> children = new LinkedList<>(mChildren);
+ for (int i = children.size() - 1; i >= 0; i--) {
+ final WindowState win = children.get(i);
destroyedSomething |= win.destroySurface(cleanupOnResume, mAppStopped);
}
if (destroyedSomething) {
@@ -704,7 +741,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
// We set the hidden state to false for the token from a transferred starting window.
// We now reset it back to true since the starting window was the last window in the
// token.
- hidden = true;
+ setHidden(true);
}
} else if (mChildren.size() == 1 && startingSurface != null && !isRelaunching()) {
// If this is the last window except for a starting transition window,
@@ -750,14 +787,6 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
final WindowState w = mChildren.get(i);
w.setWillReplaceWindow(animate);
}
- if (animate) {
- // Set-up dummy animation so we can start treating windows associated with this
- // token like they are in transition before the new app window is ready for us to
- // run the real transition animation.
- if (DEBUG_APP_TRANSITIONS) Slog.v(TAG_WM,
- "setWillReplaceWindow() Setting dummy animation on: " + this);
- mAppAnimator.setDummyAnimation();
- }
}
void setWillReplaceChildWindows() {
@@ -1001,13 +1030,12 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
void startFreezingScreen() {
if (DEBUG_ORIENTATION) logWithStack(TAG, "Set freezing of " + appToken + ": hidden="
- + hidden + " freezing=" + mAppAnimator.freezingScreen + " hiddenRequested="
+ + isHidden() + " freezing=" + mFreezingScreen + " hiddenRequested="
+ hiddenRequested);
if (!hiddenRequested) {
- if (!mAppAnimator.freezingScreen) {
- mAppAnimator.freezingScreen = true;
+ if (!mFreezingScreen) {
+ mFreezingScreen = true;
mService.registerAppFreezeListener(this);
- mAppAnimator.lastFreezeDuration = 0;
mService.mAppsFreezingScreen++;
if (mService.mAppsFreezingScreen == 1) {
mService.startFreezingDisplayLocked(false, 0, 0, getDisplayContent());
@@ -1024,7 +1052,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
void stopFreezingScreen(boolean unfreezeSurfaceNow, boolean force) {
- if (!mAppAnimator.freezingScreen) {
+ if (!mFreezingScreen) {
return;
}
if (DEBUG_ORIENTATION) Slog.v(TAG_WM, "Clear freezing of " + this + " force=" + force);
@@ -1036,10 +1064,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
if (force || unfrozeWindows) {
if (DEBUG_ORIENTATION) Slog.v(TAG_WM, "No longer freezing: " + this);
- mAppAnimator.freezingScreen = false;
+ mFreezingScreen = false;
mService.unregisterAppFreezeListener(this);
- mAppAnimator.lastFreezeDuration =
- (int)(SystemClock.elapsedRealtime() - mService.mDisplayFreezeTime);
mService.mAppsFreezingScreen--;
mService.mLastFinishedFreezeSource = this;
}
@@ -1105,14 +1131,19 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
if (fromToken.firstWindowDrawn) {
firstWindowDrawn = true;
}
- if (!fromToken.hidden) {
- hidden = false;
+ if (!fromToken.isHidden()) {
+ setHidden(false);
hiddenRequested = false;
mHiddenSetFromTransferredStartingWindow = true;
}
setClientHidden(fromToken.mClientHidden);
- fromToken.mAppAnimator.transferCurrentAnimation(
- mAppAnimator, tStartingWindow.mWinAnimator);
+
+ transferAnimation(fromToken);
+
+ // When transferring an animation, we no longer need to apply an animation to the
+ // the token we transfer the animation over. Thus, remove the animation from
+ // pending opening apps.
+ mService.mOpeningApps.remove(this);
mService.updateFocusedWindowLocked(
UPDATE_FOCUS_WILL_PLACE_SURFACES, true /*updateInputWindows*/);
@@ -1136,17 +1167,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
return true;
}
- final AppWindowAnimator tAppAnimator = fromToken.mAppAnimator;
- final AppWindowAnimator wAppAnimator = mAppAnimator;
- if (tAppAnimator.thumbnail != null) {
- // The old token is animating with a thumbnail, transfer that to the new token.
- if (wAppAnimator.thumbnail != null) {
- wAppAnimator.thumbnail.destroy();
- }
- wAppAnimator.thumbnail = tAppAnimator.thumbnail;
- wAppAnimator.thumbnailAnimation = tAppAnimator.thumbnailAnimation;
- tAppAnimator.thumbnail = null;
- }
+ // TODO: Transfer thumbnail
+
return false;
}
@@ -1154,16 +1176,6 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
return mChildren.size() == 1 && mChildren.get(0) == win;
}
- void setAllAppWinAnimators() {
- final ArrayList<WindowStateAnimator> allAppWinAnimators = mAppAnimator.mAllAppWinAnimators;
- allAppWinAnimators.clear();
-
- final int windowsCount = mChildren.size();
- for (int j = 0; j < windowsCount; j++) {
- (mChildren.get(j)).addWinAnimatorToList(allAppWinAnimators);
- }
- }
-
@Override
void onAppTransitionDone() {
sendingToBottom = false;
@@ -1200,18 +1212,18 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
@Override
void checkAppWindowsReadyToShow() {
- if (allDrawn == mAppAnimator.allDrawn) {
+ if (allDrawn == mLastAllDrawn) {
return;
}
- mAppAnimator.allDrawn = allDrawn;
+ mLastAllDrawn = allDrawn;
if (!allDrawn) {
return;
}
// The token has now changed state to having all windows shown... what to do, what to do?
- if (mAppAnimator.freezingScreen) {
- mAppAnimator.showAllWindowsLocked();
+ if (mFreezingScreen) {
+ showAllWindowsLocked();
stopFreezingScreen(false, true);
if (DEBUG_ORIENTATION) Slog.i(TAG,
"Setting mOrientationChangeComplete=true because wtoken " + this
@@ -1224,7 +1236,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
// We can now show all of the drawn windows!
if (!mService.mOpeningApps.contains(this)) {
- mService.mAnimator.orAnimating(mAppAnimator.showAllWindowsLocked());
+ showAllWindowsLocked();
}
}
}
@@ -1294,10 +1306,10 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
if (DEBUG_STARTING_WINDOW_VERBOSE && w == startingWindow) {
Slog.d(TAG, "updateWindows: starting " + w + " isOnScreen=" + w.isOnScreen()
- + " allDrawn=" + allDrawn + " freezingScreen=" + mAppAnimator.freezingScreen);
+ + " allDrawn=" + allDrawn + " freezingScreen=" + mFreezingScreen);
}
- if (allDrawn && !mAppAnimator.freezingScreen) {
+ if (allDrawn && !mFreezingScreen) {
return false;
}
@@ -1314,13 +1326,13 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
if (!allDrawn && w.mightAffectAllDrawn()) {
if (DEBUG_VISIBILITY || DEBUG_ORIENTATION) {
Slog.v(TAG, "Eval win " + w + ": isDrawn=" + w.isDrawnLw()
- + ", isAnimationSet=" + winAnimator.isAnimationSet());
+ + ", isAnimationSet=" + isSelfAnimating());
if (!w.isDrawnLw()) {
Slog.v(TAG, "Not displayed: s=" + winAnimator.mSurfaceController
+ " pv=" + w.mPolicyVisibility
+ " mDrawState=" + winAnimator.drawStateToString()
+ " ph=" + w.isParentWindowHidden() + " th=" + hiddenRequested
- + " a=" + winAnimator.mAnimating);
+ + " a=" + isSelfAnimating());
}
}
@@ -1332,7 +1344,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
if (DEBUG_VISIBILITY || DEBUG_ORIENTATION) Slog.v(TAG, "tokenMayBeDrawn: "
+ this + " w=" + w + " numInteresting=" + mNumInterestingWindows
- + " freezingScreen=" + mAppAnimator.freezingScreen
+ + " freezingScreen=" + mFreezingScreen
+ " mAppFreezing=" + w.mAppFreezing);
isInterestingAndDrawn = true;
@@ -1350,21 +1362,6 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
@Override
- void stepAppWindowsAnimation(long currentTime) {
- mAppAnimator.wasAnimating = mAppAnimator.animating;
- if (mAppAnimator.stepAnimationLocked(currentTime)) {
- mAppAnimator.animating = true;
- mService.mAnimator.setAnimating(true);
- mService.mAnimator.mAppWindowAnimating = true;
- } else if (mAppAnimator.wasAnimating) {
- // stopped animating, do one more pass through the layout
- setAppLayoutChanges(FINISH_LAYOUT_REDO_WALLPAPER,
- DEBUG_LAYOUT_REPEATS ? "appToken " + this + " done" : null);
- if (DEBUG_ANIM) Slog.v(TAG, "updateWindowsApps...: done animating " + this);
- }
- }
-
- @Override
boolean forAllWindows(ToBooleanFunction<WindowState> callback, boolean traverseTopToBottom) {
// For legacy reasons we process the TaskStack.mExitingAppTokens first in DisplayContent
// before the non-exiting app tokens. So, we skip the exiting app tokens here.
@@ -1515,13 +1512,269 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
@Override
- int getAnimLayerAdjustment() {
- return mAppAnimator.animLayerAdjustment;
+ public SurfaceControl.Builder makeAnimationLeash() {
+ return super.makeAnimationLeash()
+ .setParent(getAppAnimationLayer());
+ }
+
+ boolean applyAnimationLocked(WindowManager.LayoutParams lp, int transit, boolean enter,
+ boolean isVoiceInteraction) {
+
+ if (mService.mDisableTransitionAnimation) {
+ if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
+ Slog.v(TAG_WM, "applyAnimation: transition animation is disabled. atoken=" + this);
+ }
+ cancelAnimation();
+ return false;
+ }
+
+ // Only apply an animation if the display isn't frozen. If it is frozen, there is no reason
+ // to animate and it can cause strange artifacts when we unfreeze the display if some
+ // different animation is running.
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AWT#applyAnimationLocked");
+ if (okToAnimate()) {
+ final Animation a = loadAnimation(lp, transit, enter, isVoiceInteraction);
+ if (a != null) {
+ final TaskStack stack = getStack();
+ mTmpPoint.set(0, 0);
+ mTmpRect.setEmpty();
+ if (stack != null) {
+ stack.getRelativePosition(mTmpPoint);
+ stack.getBounds(mTmpRect);
+ }
+ final AnimationAdapter adapter = new LocalAnimationAdapter(
+ new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
+ mService.mAppTransition.canSkipFirstFrame(),
+ mService.mAppTransition.getAppStackClipMode()),
+ mService.mSurfaceAnimationRunner);
+ if (a.getZAdjustment() == Animation.ZORDER_TOP) {
+ mNeedsZBoost = true;
+ }
+ startAnimation(getPendingTransaction(), adapter, !isVisible());
+ mTransit = transit;
+ mTransitFlags = mService.mAppTransition.getTransitFlags();
+ }
+ } else {
+ cancelAnimation();
+ }
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+
+ return isReallyAnimating();
+ }
+
+ private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter,
+ boolean isVoiceInteraction) {
+ final DisplayContent displayContent = getTask().getDisplayContent();
+ final DisplayInfo displayInfo = displayContent.getDisplayInfo();
+ final int width = displayInfo.appWidth;
+ final int height = displayInfo.appHeight;
+ if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG_WM,
+ "applyAnimation: atoken=" + this);
+
+ // Determine the visible rect to calculate the thumbnail clip
+ final WindowState win = findMainWindow();
+ final Rect frame = new Rect(0, 0, width, height);
+ final Rect displayFrame = new Rect(0, 0,
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
+ final Rect insets = new Rect();
+ final Rect stableInsets = new Rect();
+ Rect surfaceInsets = null;
+ final boolean freeform = win != null && win.inFreeformWindowingMode();
+ if (win != null) {
+ // Containing frame will usually cover the whole screen, including dialog windows.
+ // For freeform workspace windows it will not cover the whole screen and it also
+ // won't exactly match the final freeform window frame (e.g. when overlapping with
+ // the status bar). In that case we need to use the final frame.
+ if (freeform) {
+ frame.set(win.mFrame);
+ } else {
+ frame.set(win.mContainingFrame);
+ }
+ surfaceInsets = win.getAttrs().surfaceInsets;
+ insets.set(win.mContentInsets);
+ stableInsets.set(win.mStableInsets);
+ }
+
+ if (mLaunchTaskBehind) {
+ // Differentiate the two animations. This one which is briefly on the screen
+ // gets the !enter animation, and the other activity which remains on the
+ // screen gets the enter animation. Both appear in the mOpeningApps set.
+ enter = false;
+ }
+ if (DEBUG_APP_TRANSITIONS) Slog.d(TAG_WM, "Loading animation for app transition."
+ + " transit=" + AppTransition.appTransitionToString(transit) + " enter=" + enter
+ + " frame=" + frame + " insets=" + insets + " surfaceInsets=" + surfaceInsets);
+ final Configuration displayConfig = displayContent.getConfiguration();
+ final Animation a = mService.mAppTransition.loadAnimation(lp, transit, enter,
+ displayConfig.uiMode, displayConfig.orientation, frame, displayFrame, insets,
+ surfaceInsets, stableInsets, isVoiceInteraction, freeform, getTask().mTaskId);
+ if (a != null) {
+ if (DEBUG_ANIM) logWithStack(TAG, "Loaded animation " + a + " for " + this);
+ final int containingWidth = frame.width();
+ final int containingHeight = frame.height();
+ a.initialize(containingWidth, containingHeight, width, height);
+ a.scaleCurrentDuration(mService.getTransitionAnimationScaleLocked());
+ }
+ return a;
+ }
+
+ @Override
+ protected void setLayer(Transaction t, int layer) {
+ if (!mSurfaceAnimator.hasLeash()) {
+ t.setLayer(mSurfaceControl, layer);
+ }
}
@Override
- void dump(PrintWriter pw, String prefix) {
- super.dump(pw, prefix);
+ protected void setRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) {
+ if (!mSurfaceAnimator.hasLeash()) {
+ t.setRelativeLayer(mSurfaceControl, relativeTo, layer);
+ }
+ }
+
+ @Override
+ protected void reparentSurfaceControl(Transaction t, SurfaceControl newParent) {
+ if (!mSurfaceAnimator.hasLeash()) {
+ t.reparent(mSurfaceControl, newParent.getHandle());
+ }
+ }
+
+ @Override
+ public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
+
+ // The leash is parented to the animation layer. We need to preserve the z-order by using
+ // the prefix order index, but we boost if necessary.
+ int layer = getPrefixOrderIndex();
+ if (mNeedsZBoost) {
+ layer += Z_BOOST_BASE;
+ }
+ leash.setLayer(layer);
+ }
+
+ /**
+ * This must be called while inside a transaction.
+ */
+ void showAllWindowsLocked() {
+ forAllWindows(windowState -> {
+ if (DEBUG_VISIBILITY) Slog.v(TAG, "performing show on: " + windowState);
+ windowState.performShowLocked();
+ }, false /* traverseTopToBottom */);
+ }
+
+ @Override
+ protected void onAnimationFinished() {
+ super.onAnimationFinished();
+
+ mTransit = TRANSIT_UNSET;
+ mTransitFlags = 0;
+ mNeedsZBoost = false;
+
+ setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM | FINISH_LAYOUT_REDO_WALLPAPER,
+ "AppWindowToken");
+
+ clearThumbnail();
+
+ if (mService.mInputMethodTarget != null && mService.mInputMethodTarget.mAppToken == this) {
+ getDisplayContent().computeImeTarget(true /* updateImeTarget */);
+ }
+
+ if (DEBUG_ANIM) Slog.v(TAG, "Animation done in " + this
+ + ": reportedVisible=" + reportedVisible
+ + " okToDisplay=" + okToDisplay()
+ + " okToAnimate=" + okToAnimate()
+ + " startingDisplayed=" + startingDisplayed);
+
+ // WindowState.onExitAnimationDone might modify the children list, so make a copy and then
+ // traverse the copy.
+ final ArrayList<WindowState> children = new ArrayList<>(mChildren);
+ children.forEach(WindowState::onExitAnimationDone);
+
+ mService.mAppTransition.notifyAppTransitionFinishedLocked(token);
+ scheduleAnimation();
+ }
+
+ @Override
+ boolean isAppAnimating() {
+ return isSelfAnimating();
+ }
+
+ @Override
+ boolean isSelfAnimating() {
+ // If we are about to start a transition, we also need to be considered animating.
+ return isWaitingForTransitionStart() || isReallyAnimating();
+ }
+
+ /**
+ * @return True if and only if we are actually running an animation. Note that
+ * {@link #isSelfAnimating} also returns true if we are waiting for an animation to
+ * start.
+ */
+ private boolean isReallyAnimating() {
+ return super.isSelfAnimating();
+ }
+
+ @Override
+ void cancelAnimation() {
+ super.cancelAnimation();
+ clearThumbnail();
+ }
+
+ boolean isWaitingForTransitionStart() {
+ return mService.mAppTransition.isTransitionSet()
+ && (mService.mOpeningApps.contains(this) || mService.mClosingApps.contains(this));
+ }
+
+ public int getTransit() {
+ return mTransit;
+ }
+
+ int getTransitFlags() {
+ return mTransitFlags;
+ }
+
+ void attachThumbnailAnimation() {
+ if (!isReallyAnimating()) {
+ return;
+ }
+ final int taskId = getTask().mTaskId;
+ final GraphicBuffer thumbnailHeader =
+ mService.mAppTransition.getAppTransitionThumbnailHeader(taskId);
+ if (thumbnailHeader == null) {
+ if (DEBUG_APP_TRANSITIONS) Slog.d(TAG, "No thumbnail header bitmap for: " + taskId);
+ return;
+ }
+ clearThumbnail();
+ mThumbnail = new AppWindowThumbnail(getPendingTransaction(), this, thumbnailHeader);
+ mThumbnail.startAnimation(getPendingTransaction(), loadThumbnailAnimation(thumbnailHeader));
+ }
+
+ private Animation loadThumbnailAnimation(GraphicBuffer thumbnailHeader) {
+ final DisplayInfo displayInfo = mDisplayContent.getDisplayInfo();
+
+ // If this is a multi-window scenario, we use the windows frame as
+ // destination of the thumbnail header animation. If this is a full screen
+ // window scenario, we use the whole display as the target.
+ WindowState win = findMainWindow();
+ Rect appRect = win != null ? win.getContentFrameLw() :
+ new Rect(0, 0, displayInfo.appWidth, displayInfo.appHeight);
+ Rect insets = win != null ? win.mContentInsets : null;
+ final Configuration displayConfig = mDisplayContent.getConfiguration();
+ return mService.mAppTransition.createThumbnailAspectScaleAnimationLocked(
+ appRect, insets, thumbnailHeader, getTask().mTaskId, displayConfig.uiMode,
+ displayConfig.orientation);
+ }
+
+ private void clearThumbnail() {
+ if (mThumbnail == null) {
+ return;
+ }
+ mThumbnail.destroy();
+ mThumbnail = null;
+ }
+
+ @Override
+ void dump(PrintWriter pw, String prefix, boolean dumpAll) {
+ super.dump(pw, prefix, dumpAll);
if (appToken != null) {
pw.println(prefix + "app=true mVoiceInteraction=" + mVoiceInteraction);
}
@@ -1538,13 +1791,13 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
pw.print(prefix); pw.print("mAppStopped="); pw.println(mAppStopped);
}
if (mNumInterestingWindows != 0 || mNumDrawnWindows != 0
- || allDrawn || mAppAnimator.allDrawn) {
+ || allDrawn || mLastAllDrawn) {
pw.print(prefix); pw.print("mNumInterestingWindows=");
pw.print(mNumInterestingWindows);
pw.print(" mNumDrawnWindows="); pw.print(mNumDrawnWindows);
pw.print(" inPendingTransaction="); pw.print(inPendingTransaction);
pw.print(" allDrawn="); pw.print(allDrawn);
- pw.print(" (animator="); pw.print(mAppAnimator.allDrawn);
+ pw.print(" lastAllDrawn="); pw.print(mLastAllDrawn);
pw.println(")");
}
if (inPendingTransaction) {
@@ -1579,9 +1832,39 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
if (mRemovingFromDisplay) {
pw.println(prefix + "mRemovingFromDisplay=" + mRemovingFromDisplay);
}
- if (mAppAnimator.isAnimating()) {
- mAppAnimator.dump(pw, prefix + " ");
+ }
+
+ @Override
+ void setHidden(boolean hidden) {
+ super.setHidden(hidden);
+ scheduleAnimation();
+ }
+
+ @Override
+ void prepareSurfaces() {
+ // isSelfAnimating also returns true when we are about to start a transition, so we need
+ // to check super here.
+ final boolean reallyAnimating = super.isSelfAnimating();
+ final boolean show = !isHidden() || reallyAnimating;
+ if (show && !mLastSurfaceShowing) {
+ mPendingTransaction.show(mSurfaceControl);
+ } else if (!show && mLastSurfaceShowing) {
+ mPendingTransaction.hide(mSurfaceControl);
+ }
+ if (mThumbnail != null) {
+ mThumbnail.setShowing(mPendingTransaction, show);
}
+ mLastSurfaceShowing = show;
+ super.prepareSurfaces();
+ }
+
+ boolean isFreezingScreen() {
+ return mFreezingScreen;
+ }
+
+ @Override
+ boolean needsZBoost() {
+ return mNeedsZBoost || super.needsZBoost();
}
@CallSuper
diff --git a/com/android/server/wm/DisplayContent.java b/com/android/server/wm/DisplayContent.java
index 41348ba4..d053015c 100644
--- a/com/android/server/wm/DisplayContent.java
+++ b/com/android/server/wm/DisplayContent.java
@@ -69,7 +69,6 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_FOCUS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_FOCUS_LIGHT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_INPUT_METHOD;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYERS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ORIENTATION;
@@ -91,8 +90,6 @@ import static com.android.server.wm.WindowManagerService.H.WINDOW_HIDE_TIMEOUT;
import static com.android.server.wm.WindowManagerService.LAYOUT_REPEAT_THRESHOLD;
import static com.android.server.wm.WindowManagerService.MAX_ANIMATION_DURATION;
import static com.android.server.wm.WindowManagerService.SEAMLESS_ROTATION_TIMEOUT_DURATION;
-import static com.android.server.wm.WindowManagerService.TYPE_LAYER_MULTIPLIER;
-import static com.android.server.wm.WindowManagerService.TYPE_LAYER_OFFSET;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES;
import static com.android.server.wm.WindowManagerService.WINDOWS_FREEZING_SCREENS_ACTIVE;
import static com.android.server.wm.WindowManagerService.WINDOWS_FREEZING_SCREENS_TIMEOUT;
@@ -123,7 +120,6 @@ import android.content.pm.PackageManager;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
-import android.graphics.GraphicBuffer;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -135,6 +131,7 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.Trace;
import android.util.DisplayMetrics;
import android.util.MutableBoolean;
import android.util.Slog;
@@ -145,6 +142,7 @@ import android.view.InputDevice;
import android.view.MagnificationSpec;
import android.view.Surface;
import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
import android.view.SurfaceSession;
import com.android.internal.annotations.VisibleForTesting;
@@ -178,20 +176,20 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
/** The containers below are the only child containers the display can have. */
// Contains all window containers that are related to apps (Activities)
- private final TaskStackContainers mTaskStackContainers = new TaskStackContainers();
+ private final TaskStackContainers mTaskStackContainers = new TaskStackContainers(mService);
// Contains all non-app window containers that should be displayed above the app containers
// (e.g. Status bar)
private final AboveAppWindowContainers mAboveAppWindowsContainers =
- new AboveAppWindowContainers("mAboveAppWindowsContainers");
+ new AboveAppWindowContainers("mAboveAppWindowsContainers", mService);
// Contains all non-app window containers that should be displayed below the app containers
// (e.g. Wallpaper).
private final NonAppWindowContainers mBelowAppWindowsContainers =
- new NonAppWindowContainers("mBelowAppWindowsContainers");
+ new NonAppWindowContainers("mBelowAppWindowsContainers", mService);
// Contains all IME window containers. Note that the z-ordering of the IME windows will depend
// on the IME target. We mainly have this container grouping so we can keep track of all the IME
// window containers together and move them in-sync if/when needed.
private final NonAppWindowContainers mImeWindowsContainers =
- new NonAppWindowContainers("mImeWindowsContainers");
+ new NonAppWindowContainers("mImeWindowsContainers", mService);
private WindowState mTmpWindow;
private WindowState mTmpWindow2;
@@ -317,8 +315,6 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
/** Used for handing back size of display */
private final Rect mTmpBounds = new Rect();
- WindowManagerService mService;
-
/** Remove this display when animation on it has completed. */
private boolean mDeferredRemoval;
@@ -340,6 +336,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
new ApplySurfaceChangesTransactionState();
private final ScreenshotApplicationState mScreenshotApplicationState =
new ScreenshotApplicationState();
+ private final Transaction mTmpTransaction = new Transaction();
// True if this display is in the process of being removed. Used to determine if the removal of
// the display's direct children should be allowed.
@@ -349,7 +346,6 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
private boolean mDisplayReady = false;
WallpaperController mWallpaperController;
- int mInputMethodAnimLayerAdjustment;
private final SurfaceSession mSession = new SurfaceSession();
@@ -380,29 +376,11 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
*/
private final ArrayList<SurfaceControl> mPendingDestroyingSurfaces = new ArrayList<>();
+ /** Temporary float array to retrieve 3x3 matrix values. */
+ private final float[] mTmpFloats = new float[9];
+
private final Consumer<WindowState> mUpdateWindowsForAnimator = w -> {
WindowStateAnimator winAnimator = w.mWinAnimator;
- if (winAnimator.hasSurface()) {
- final boolean wasAnimating = winAnimator.mWasAnimating;
- final boolean nowAnimating = winAnimator.stepAnimationLocked(
- mTmpWindowAnimator.mCurrentTime);
- winAnimator.mWasAnimating = nowAnimating;
- mTmpWindowAnimator.orAnimating(nowAnimating);
-
- if (DEBUG_WALLPAPER) Slog.v(TAG,
- w + ": wasAnimating=" + wasAnimating + ", nowAnimating=" + nowAnimating);
-
- if (wasAnimating && !winAnimator.mAnimating
- && mWallpaperController.isWallpaperTarget(w)) {
- mTmpWindowAnimator.mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE;
- pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
- if (DEBUG_LAYOUT_REPEATS) {
- mService.mWindowPlacerLocked.debugLayoutRepeats(
- "updateWindowsAndWallpaperLocked 2", pendingLayoutChanges);
- }
- }
- }
-
final AppWindowToken atoken = w.mAppToken;
if (winAnimator.mDrawState == READY_TO_SHOW) {
if (atoken == null || atoken.allDrawn) {
@@ -415,14 +393,6 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
}
}
}
- final AppWindowAnimator appAnimator = winAnimator.mAppAnimator;
- if (appAnimator != null && appAnimator.thumbnail != null) {
- if (appAnimator.thumbnailTransactionSeq
- != mTmpWindowAnimator.mAnimTransactionSequence) {
- appAnimator.thumbnailTransactionSeq =
- mTmpWindowAnimator.mAnimTransactionSequence;
- }
- }
};
private final Consumer<WindowState> mUpdateWallpaperForAnimator = w -> {
@@ -435,13 +405,13 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
// If this window is animating, make a note that we have an animating window and take
// care of a request to run a detached wallpaper animation.
- if (winAnimator.mAnimating) {
- if (winAnimator.mAnimation != null) {
- if ((flags & FLAG_SHOW_WALLPAPER) != 0
- && winAnimator.mAnimation.getDetachWallpaper()) {
+ if (winAnimator.isAnimationSet()) {
+ final AnimationAdapter anim = w.getAnimation();
+ if (anim != null) {
+ if ((flags & FLAG_SHOW_WALLPAPER) != 0 && anim.getDetachWallpaper()) {
mTmpWindow = w;
}
- final int color = winAnimator.mAnimation.getBackgroundColor();
+ final int color = anim.getBackgroundColor();
if (color != 0) {
final TaskStack stack = w.getStack();
if (stack != null) {
@@ -449,20 +419,18 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
}
}
}
- mTmpWindowAnimator.setAnimating(true);
}
// If this window's app token is running a detached wallpaper animation, make a note so
// we can ensure the wallpaper is displayed behind it.
- final AppWindowAnimator appAnimator = winAnimator.mAppAnimator;
- if (appAnimator != null && appAnimator.animation != null
- && appAnimator.animating) {
- if ((flags & FLAG_SHOW_WALLPAPER) != 0
- && appAnimator.animation.getDetachWallpaper()) {
+ final AppWindowToken atoken = winAnimator.mWin.mAppToken;
+ final AnimationAdapter animation = atoken != null ? atoken.getAnimation() : null;
+ if (animation != null) {
+ if ((flags & FLAG_SHOW_WALLPAPER) != 0 && animation.getDetachWallpaper()) {
mTmpWindow = w;
}
- final int color = appAnimator.animation.getBackgroundColor();
+ final int color = animation.getBackgroundColor();
if (color != 0) {
final TaskStack stack = w.getStack();
if (stack != null) {
@@ -545,11 +513,11 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
+ " screen changed=" + w.isConfigChanged());
final AppWindowToken atoken = w.mAppToken;
if (gone) Slog.v(TAG, " GONE: mViewVisibility=" + w.mViewVisibility
- + " mRelayoutCalled=" + w.mRelayoutCalled + " hidden=" + w.mToken.hidden
+ + " mRelayoutCalled=" + w.mRelayoutCalled + " hidden=" + w.mToken.isHidden()
+ " hiddenRequested=" + (atoken != null && atoken.hiddenRequested)
+ " parentHidden=" + w.isParentWindowHidden());
else Slog.v(TAG, " VIS: mViewVisibility=" + w.mViewVisibility
- + " mRelayoutCalled=" + w.mRelayoutCalled + " hidden=" + w.mToken.hidden
+ + " mRelayoutCalled=" + w.mRelayoutCalled + " hidden=" + w.mToken.isHidden()
+ " hiddenRequested=" + (atoken != null && atoken.hiddenRequested)
+ " parentHidden=" + w.isParentWindowHidden());
}
@@ -685,7 +653,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mWallpaperController.updateWallpaperVisibility();
}
- w.handleWindowMovedIfNeeded();
+ // Use mTmpTransaction instead of mPendingTransaction because we don't want to commit
+ // other changes in mPendingTransaction at this point.
+ w.handleWindowMovedIfNeeded(mTmpTransaction);
+ SurfaceControl.mergeToGlobalTransaction(mTmpTransaction);
final WindowStateAnimator winAnimator = w.mWinAnimator;
@@ -721,7 +692,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
}
}
final TaskStack stack = w.getStack();
- if ((!winAnimator.isAnimationStarting() && !winAnimator.isWaitingForOpening())
+ if (!winAnimator.isWaitingForOpening()
|| (stack != null && stack.isAnimatingBounds())) {
// Updates the shown frame before we set up the surface. This is needed
// because the resizing could change the top-left position (in addition to
@@ -737,6 +708,16 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
winAnimator.computeShownFrameLocked();
}
winAnimator.setSurfaceBoundariesLocked(mTmpRecoveringMemory /* recoveringMemory */);
+
+ // Since setSurfaceBoundariesLocked applies the clipping, we need to apply the position
+ // to the surface of the window container and also the position of the stack window
+ // container as well. Use mTmpTransaction instead of mPendingTransaction to avoid
+ // committing any existing changes in there.
+ w.updateSurfacePosition(mTmpTransaction);
+ if (stack != null) {
+ stack.updateSurfaceBounds(mTmpTransaction);
+ }
+ SurfaceControl.mergeToGlobalTransaction(mTmpTransaction);
}
final AppWindowToken atoken = w.mAppToken;
@@ -765,6 +746,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
*/
DisplayContent(Display display, WindowManagerService service,
WallpaperController wallpaperController) {
+ super(service);
if (service.mRoot.getDisplayContent(display.getDisplayId()) != null) {
throw new IllegalArgumentException("Display with ID=" + display.getDisplayId()
+ " already exists=" + service.mRoot.getDisplayContent(display.getDisplayId())
@@ -777,7 +759,6 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
display.getDisplayInfo(mDisplayInfo);
display.getMetrics(mDisplayMetrics);
isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;
- mService = service;
mDisplayFrames = new DisplayFrames(mDisplayId, mDisplayInfo);
initializeDisplayBaseInfo();
mDividerControllerLocked = new DockedStackDividerController(service, this);
@@ -1767,8 +1748,8 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
updateBounds();
}
- void getContentRect(Rect out) {
- out.set(mDisplayFrames.mContent);
+ void getStableRect(Rect out) {
+ out.set(mDisplayFrames.mStable);
}
TaskStack createStack(int stackId, boolean onTop, StackWindowController controller) {
@@ -2065,12 +2046,6 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mPinnedStackControllerLocked.setAdjustedForIme(imeVisible, imeHeight);
}
- void setInputMethodAnimLayerAdjustment(int adj) {
- if (DEBUG_LAYERS) Slog.v(TAG_WM, "Setting im layer adj to " + adj);
- mInputMethodAnimLayerAdjustment = adj;
- assignWindowLayers(false /* relayoutNeeded */);
- }
-
/**
* If a window that has an animation specifying a colored background and the current wallpaper
* is visible, then the color goes *below* the wallpaper so we don't cause the wallpaper to
@@ -2177,7 +2152,9 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
proto.end(token);
}
- public void dump(String prefix, PrintWriter pw) {
+ @Override
+ public void dump(PrintWriter pw, String prefix, boolean dumpAll) {
+ super.dump(pw, prefix, dumpAll);
pw.print(prefix); pw.print("Display: mDisplayId="); pw.println(mDisplayId);
final String subPrefix = " " + prefix;
pw.print(subPrefix); pw.print("init="); pw.print(mInitialDisplayWidth); pw.print("x");
@@ -2211,7 +2188,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
pw.println(prefix + "Application tokens in top down Z order:");
for (int stackNdx = mTaskStackContainers.getChildCount() - 1; stackNdx >= 0; --stackNdx) {
final TaskStack stack = mTaskStackContainers.getChildAt(stackNdx);
- stack.dump(prefix + " ", pw);
+ stack.dump(pw, prefix + " ", dumpAll);
}
pw.println();
@@ -2223,7 +2200,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
pw.print(" Exiting #"); pw.print(i);
pw.print(' '); pw.print(token);
pw.println(':');
- token.dump(pw, " ");
+ token.dump(pw, " ", dumpAll);
}
}
@@ -2248,11 +2225,6 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
pw.println();
mPinnedStackControllerLocked.dump(prefix, pw);
- if (mInputMethodAnimLayerAdjustment != 0) {
- pw.println(subPrefix
- + "mInputMethodAnimLayerAdjustment=" + mInputMethodAnimLayerAdjustment);
- }
-
pw.println();
mDisplayFrames.dump(prefix, pw);
}
@@ -2339,6 +2311,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
/** Updates the layer assignment of windows on this display. */
void assignWindowLayers(boolean setLayoutNeeded) {
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "assignWindowLayers");
assignChildLayers(getPendingTransaction());
if (setLayoutNeeded) {
setLayoutNeeded();
@@ -2349,6 +2322,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
// prepareSurfaces. This allows us to synchronize Z-ordering changes with
// the hiding and showing of surfaces.
scheduleAnimation();
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
}
// TODO: This should probably be called any time a visual change is made to the hierarchy like
@@ -2410,7 +2384,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
if (updateImeTarget) {
if (DEBUG_INPUT_METHOD) Slog.w(TAG_WM, "Moving IM target from "
+ mService.mInputMethodTarget + " to null since mInputMethodWindow is null");
- setInputMethodTarget(null, mService.mInputMethodTargetWaitingAnim, 0);
+ setInputMethodTarget(null, mService.mInputMethodTargetWaitingAnim);
}
return null;
}
@@ -2459,7 +2433,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
if (DEBUG_INPUT_METHOD) Slog.w(TAG_WM, "Moving IM target from " + curTarget
+ " to null." + (SHOW_STACK_CRAWLS ? " Callers="
+ Debug.getCallers(4) : ""));
- setInputMethodTarget(null, mService.mInputMethodTargetWaitingAnim, 0);
+ setInputMethodTarget(null, mService.mInputMethodTargetWaitingAnim);
}
return null;
@@ -2473,7 +2447,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
// to look at all windows below the current target that are in this app, finding the
// highest visible one in layering.
WindowState highestTarget = null;
- if (token.mAppAnimator.animating || token.mAppAnimator.animation != null) {
+ if (token.isSelfAnimating()) {
highestTarget = token.getHighestAnimLayerWindow(curTarget);
}
@@ -2487,14 +2461,14 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
if (appTransition.isTransitionSet()) {
// If we are currently setting up for an animation, hold everything until we
// can find out what will happen.
- setInputMethodTarget(highestTarget, true, mInputMethodAnimLayerAdjustment);
+ setInputMethodTarget(highestTarget, true);
return highestTarget;
} else if (highestTarget.mWinAnimator.isAnimationSet() &&
highestTarget.mWinAnimator.mAnimLayer > target.mWinAnimator.mAnimLayer) {
// If the window we are currently targeting is involved with an animation,
// and it is on top of the next target we will be over, then hold off on
// moving until that is done.
- setInputMethodTarget(highestTarget, true, mInputMethodAnimLayerAdjustment);
+ setInputMethodTarget(highestTarget, true);
return highestTarget;
}
}
@@ -2502,23 +2476,20 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
if (DEBUG_INPUT_METHOD) Slog.w(TAG_WM, "Moving IM target from " + curTarget + " to "
+ target + (SHOW_STACK_CRAWLS ? " Callers=" + Debug.getCallers(4) : ""));
- setInputMethodTarget(target, false, target.mAppToken != null
- ? target.mAppToken.getAnimLayerAdjustment() : 0);
+ setInputMethodTarget(target, false);
}
return target;
}
- private void setInputMethodTarget(WindowState target, boolean targetWaitingAnim, int layerAdj) {
+ private void setInputMethodTarget(WindowState target, boolean targetWaitingAnim) {
if (target == mService.mInputMethodTarget
- && mService.mInputMethodTargetWaitingAnim == targetWaitingAnim
- && mInputMethodAnimLayerAdjustment == layerAdj) {
+ && mService.mInputMethodTargetWaitingAnim == targetWaitingAnim) {
return;
}
mService.mInputMethodTarget = target;
mService.mInputMethodTargetWaitingAnim = targetWaitingAnim;
- setInputMethodAnimLayerAdjustment(layerAdj);
assignWindowLayers(false /* setLayoutNeeded */);
}
@@ -2580,7 +2551,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
pw.print(token);
if (dumpAll) {
pw.println(':');
- token.dump(pw, " ");
+ token.dump(pw, " ", dumpAll);
} else {
pw.println();
}
@@ -2616,8 +2587,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
forAllWindows(w -> {
if (w.mAppToken == null && policy.canBeHiddenByKeyguardLw(w)
&& w.wouldBeVisibleIfPolicyIgnored() && !w.isVisible()) {
- w.mWinAnimator.setAnimation(
- policy.createHiddenByKeyguardExit(onWallpaper, goingToShade));
+ w.startAnimation(policy.createHiddenByKeyguardExit(onWallpaper, goingToShade));
}
}, true /* traverseTopToBottom */);
}
@@ -3182,6 +3152,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
*/
static class DisplayChildWindowContainer<E extends WindowContainer> extends WindowContainer<E> {
+ DisplayChildWindowContainer(WindowManagerService service) {
+ super(service);
+ }
+
@Override
boolean fillsParent() {
return true;
@@ -3201,7 +3175,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
/**
* A control placed at the appropriate level for transitions to occur.
*/
- SurfaceControl mAnimationLayer = null;
+ SurfaceControl mAppAnimationLayer = null;
// Cached reference to some special stacks we tend to get a lot so we don't need to loop
// through the list to find them.
@@ -3209,6 +3183,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
private TaskStack mPinnedStack = null;
private TaskStack mSplitScreenPrimaryStack = null;
+ TaskStackContainers(WindowManagerService service) {
+ super(service);
+ }
+
/**
* Returns the topmost stack on the display that is compatible with the input windowing mode
* and activity type. Null is no compatible stack on the display.
@@ -3462,8 +3440,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
// Make sure there is no animation running on this token, so any windows
// associated with it will be removed as soon as their animations are
// complete.
- token.mAppAnimator.clearAnimation();
- token.mAppAnimator.animating = false;
+ cancelAnimation();
if (DEBUG_ADD_REMOVE || DEBUG_TOKEN_MOVEMENT) Slog.v(TAG,
"performLayout: App token exiting now removed" + token);
token.removeIfPossible();
@@ -3516,52 +3493,63 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
@Override
void assignChildLayers(SurfaceControl.Transaction t) {
- final int NORMAL_STACK_STATE = 0;
- final int BOOSTED_STATE = 1;
- final int ALWAYS_ON_TOP_STATE = 2;
+ int layer = 0;
// We allow stacks to change visual order from the AM specified order due to
// Z-boosting during animations. However we must take care to ensure TaskStacks
// which are marked as alwaysOnTop remain that way.
- int layer = 0;
- for (int state = 0; state <= ALWAYS_ON_TOP_STATE; state++) {
- for (int i = 0; i < mChildren.size(); i++) {
- final TaskStack s = mChildren.get(i);
- layer++;
- if (state == NORMAL_STACK_STATE) {
- s.assignLayer(t, layer);
- } else if (state == BOOSTED_STATE && s.needsZBoost()) {
- s.assignLayer(t, layer);
- } else if (state == ALWAYS_ON_TOP_STATE &&
- s.isAlwaysOnTop()) {
- s.assignLayer(t, layer);
- }
- s.assignChildLayers(t);
+ for (int i = 0; i < mChildren.size(); i++) {
+ final TaskStack s = mChildren.get(i);
+ s.assignChildLayers();
+ if (!s.needsZBoost() && !s.isAlwaysOnTop()) {
+ s.assignLayer(t, layer++);
}
- // The appropriate place for App-Transitions to occur is right
- // above all other animations but still below things in the Picture-and-Picture
- // windowing mode.
- if (state == BOOSTED_STATE && mAnimationLayer != null) {
- t.setLayer(mAnimationLayer, layer + 1);
+ }
+ for (int i = 0; i < mChildren.size(); i++) {
+ final TaskStack s = mChildren.get(i);
+ if (s.needsZBoost() && !s.isAlwaysOnTop()) {
+ s.assignLayer(t, layer++);
}
}
+ for (int i = 0; i < mChildren.size(); i++) {
+ final TaskStack s = mChildren.get(i);
+ if (s.isAlwaysOnTop()) {
+ s.assignLayer(t, layer++);
+ }
+ }
+
+ // The appropriate place for App-Transitions to occur is right
+ // above all other animations but still below things in the Picture-and-Picture
+ // windowing mode.
+ if (mAppAnimationLayer != null) {
+ t.setLayer(mAppAnimationLayer, layer++);
+ }
+ }
+
+ @Override
+ SurfaceControl getAppAnimationLayer() {
+ return mAppAnimationLayer;
}
@Override
void onParentSet() {
super.onParentSet();
if (getParent() != null) {
- mAnimationLayer = makeSurface().build();
+ mAppAnimationLayer = makeChildSurface(null)
+ .setName("animationLayer")
+ .build();
+ getPendingTransaction().show(mAppAnimationLayer);
+ scheduleAnimation();
} else {
- mAnimationLayer.destroy();
- mAnimationLayer = null;
+ mAppAnimationLayer.destroy();
+ mAppAnimationLayer = null;
}
}
}
private final class AboveAppWindowContainers extends NonAppWindowContainers {
- AboveAppWindowContainers(String name) {
- super(name);
+ AboveAppWindowContainers(String name, WindowManagerService service) {
+ super(name, service);
}
void assignChildLayers(SurfaceControl.Transaction t, WindowContainer imeContainer) {
@@ -3574,15 +3562,15 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
int layer = mService.mPolicy.getWindowLayerFromTypeLw(
wt.windowType, wt.mOwnerCanManageAppTokens);
- if (needAssignIme && layer >= TYPE_INPUT_METHOD_DIALOG) {
- t.setRelativeLayer(imeContainer.getSurfaceControl(),
- wt.getSurfaceControl(), -1);
+
+ if (needAssignIme && layer >= mService.mPolicy.getWindowLayerFromTypeLw(
+ TYPE_INPUT_METHOD_DIALOG, true)) {
+ imeContainer.assignRelativeLayer(t, wt.getSurfaceControl(), -1);
needAssignIme = false;
}
}
if (needAssignIme) {
- t.setRelativeLayer(imeContainer.getSurfaceControl(),
- getSurfaceControl(), Integer.MIN_VALUE);
+ imeContainer.assignRelativeLayer(t, getSurfaceControl(), Integer.MAX_VALUE);
}
}
}
@@ -3616,7 +3604,8 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
};
private final String mName;
- NonAppWindowContainers(String name) {
+ NonAppWindowContainers(String name, WindowManagerService service) {
+ super(service);
mName = name;
}
@@ -3710,8 +3699,6 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
@Override
void assignChildLayers(SurfaceControl.Transaction t) {
- t.setLayer(mOverlayLayer, 1)
- .setLayer(mWindowingLayer, 0);
// These are layers as children of "mWindowingLayer"
mBelowAppWindowsContainers.assignLayer(t, 0);
@@ -3735,8 +3722,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
// place it in the AboveAppWindowContainers.
if (imeTarget != null && !imeTarget.inSplitScreenWindowingMode()
&& (imeTarget.getSurfaceControl() != null)) {
- t.setRelativeLayer(mImeWindowsContainers.getSurfaceControl(),
- imeTarget.getSurfaceControl(),
+ mImeWindowsContainers.assignRelativeLayer(t, imeTarget.getSurfaceControl(),
// TODO: We need to use an extra level on the app surface to ensure
// this is always above SurfaceView but always below attached window.
1);
@@ -3766,13 +3752,13 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
}
@Override
- void destroyAfterPendingTransaction(SurfaceControl surface) {
+ public void destroyAfterPendingTransaction(SurfaceControl surface) {
mPendingDestroyingSurfaces.add(surface);
}
/**
* Destroys any surfaces that have been put into the pending list with
- * {@link #destroyAfterTransaction}.
+ * {@link #destroyAfterPendingTransaction}.
*/
void onPendingTransactionApplied() {
for (int i = mPendingDestroyingSurfaces.size() - 1; i >= 0; i--) {
@@ -3780,4 +3766,21 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
}
mPendingDestroyingSurfaces.clear();
}
+
+ @Override
+ void prepareSurfaces() {
+ final ScreenRotationAnimation screenRotationAnimation =
+ mService.mAnimator.getScreenRotationAnimationLocked(mDisplayId);
+ if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()) {
+ screenRotationAnimation.getEnterTransformation().getMatrix().getValues(mTmpFloats);
+ mPendingTransaction.setMatrix(mWindowingLayer,
+ mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y],
+ mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]);
+ mPendingTransaction.setPosition(mWindowingLayer,
+ mTmpFloats[Matrix.MTRANS_X], mTmpFloats[Matrix.MTRANS_Y]);
+ mPendingTransaction.setAlpha(mWindowingLayer,
+ screenRotationAnimation.getEnterTransformation().getAlpha());
+ }
+ super.prepareSurfaces();
+ }
}
diff --git a/com/android/server/wm/DisplayFrames.java b/com/android/server/wm/DisplayFrames.java
index 0249713e..01557125 100644
--- a/com/android/server/wm/DisplayFrames.java
+++ b/com/android/server/wm/DisplayFrames.java
@@ -21,11 +21,17 @@ import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
import static com.android.server.wm.proto.DisplayFramesProto.STABLE_BOUNDS;
+import android.annotation.NonNull;
+import android.graphics.Point;
import android.graphics.Rect;
import android.util.proto.ProtoOutputStream;
+import android.view.DisplayCutout;
import android.view.DisplayInfo;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.io.PrintWriter;
+import java.util.Arrays;
/**
* Container class for all the display frames that affect how we do window layout on a display.
@@ -94,6 +100,14 @@ public class DisplayFrames {
/** During layout, the current screen borders along which input method windows are placed. */
public final Rect mDock = new Rect();
+ /** Definition of the cutout */
+ @NonNull public DisplayCutout mDisplayCutout = DisplayCutout.NO_CUTOUT;
+
+ /**
+ * During layout, the frame that is display-cutout safe, i.e. that does not intersect with it.
+ */
+ public final Rect mDisplayCutoutSafe = new Rect();
+
private final Rect mDisplayInfoOverscan = new Rect();
private final Rect mRotatedDisplayInfoOverscan = new Rect();
public int mDisplayWidth;
@@ -114,7 +128,7 @@ public class DisplayFrames {
info.overscanLeft, info.overscanTop, info.overscanRight, info.overscanBottom);
}
- public void onBeginLayout() {
+ public void onBeginLayout(boolean emulateDisplayCutout, int statusBarHeight) {
switch (mRotation) {
case ROTATION_90:
mRotatedDisplayInfoOverscan.left = mDisplayInfoOverscan.top;
@@ -152,13 +166,67 @@ public class DisplayFrames {
mStable.set(mUnrestricted);
mStableFullscreen.set(mUnrestricted);
mCurrent.set(mUnrestricted);
-
+ mDisplayCutout = DisplayCutout.NO_CUTOUT;
+ mDisplayCutoutSafe.set(Integer.MIN_VALUE, Integer.MIN_VALUE,
+ Integer.MAX_VALUE, Integer.MAX_VALUE);
+ if (emulateDisplayCutout) {
+ setEmulatedDisplayCutout((int) (statusBarHeight * 0.8));
+ }
}
public int getInputMethodWindowVisibleHeight() {
return mDock.bottom - mCurrent.bottom;
}
+ private void setEmulatedDisplayCutout(int height) {
+ final boolean swappedDimensions = mRotation == ROTATION_90 || mRotation == ROTATION_270;
+
+ final int screenWidth = swappedDimensions ? mDisplayHeight : mDisplayWidth;
+ final int screenHeight = swappedDimensions ? mDisplayWidth : mDisplayHeight;
+
+ final int widthTop = (int) (screenWidth * 0.3);
+ final int widthBottom = widthTop - height;
+
+ switch (mRotation) {
+ case ROTATION_90:
+ mDisplayCutout = DisplayCutout.fromBoundingPolygon(Arrays.asList(
+ new Point(0, (screenWidth - widthTop) / 2),
+ new Point(height, (screenWidth - widthBottom) / 2),
+ new Point(height, (screenWidth + widthBottom) / 2),
+ new Point(0, (screenWidth + widthTop) / 2)
+ )).calculateRelativeTo(mUnrestricted);
+ mDisplayCutoutSafe.left = height;
+ break;
+ case ROTATION_180:
+ mDisplayCutout = DisplayCutout.fromBoundingPolygon(Arrays.asList(
+ new Point((screenWidth - widthTop) / 2, screenHeight),
+ new Point((screenWidth - widthBottom) / 2, screenHeight - height),
+ new Point((screenWidth + widthBottom) / 2, screenHeight - height),
+ new Point((screenWidth + widthTop) / 2, screenHeight)
+ )).calculateRelativeTo(mUnrestricted);
+ mDisplayCutoutSafe.bottom = screenHeight - height;
+ break;
+ case ROTATION_270:
+ mDisplayCutout = DisplayCutout.fromBoundingPolygon(Arrays.asList(
+ new Point(screenHeight, (screenWidth - widthTop) / 2),
+ new Point(screenHeight - height, (screenWidth - widthBottom) / 2),
+ new Point(screenHeight - height, (screenWidth + widthBottom) / 2),
+ new Point(screenHeight, (screenWidth + widthTop) / 2)
+ )).calculateRelativeTo(mUnrestricted);
+ mDisplayCutoutSafe.right = screenHeight - height;
+ break;
+ default:
+ mDisplayCutout = DisplayCutout.fromBoundingPolygon(Arrays.asList(
+ new Point((screenWidth - widthTop) / 2, 0),
+ new Point((screenWidth - widthBottom) / 2, height),
+ new Point((screenWidth + widthBottom) / 2, height),
+ new Point((screenWidth + widthTop) / 2, 0)
+ )).calculateRelativeTo(mUnrestricted);
+ mDisplayCutoutSafe.top = height;
+ break;
+ }
+ }
+
public void writeToProto(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
mStable.writeToProto(proto, STABLE_BOUNDS);
@@ -182,6 +250,7 @@ public class DisplayFrames {
dumpFrame(mUnrestricted, "mUnrestricted", myPrefix, pw);
dumpFrame(mDisplayInfoOverscan, "mDisplayInfoOverscan", myPrefix, pw);
dumpFrame(mRotatedDisplayInfoOverscan, "mRotatedDisplayInfoOverscan", myPrefix, pw);
+ pw.println(myPrefix + "mDisplayCutout=" + mDisplayCutout);
}
private void dumpFrame(Rect frame, String name, String prefix, PrintWriter pw) {
diff --git a/com/android/server/wm/DockedStackDividerController.java b/com/android/server/wm/DockedStackDividerController.java
index a37598e7..03c0768c 100644
--- a/com/android/server/wm/DockedStackDividerController.java
+++ b/com/android/server/wm/DockedStackDividerController.java
@@ -751,10 +751,10 @@ public class DockedStackDividerController {
// There might be an old window delaying the animation start - clear it.
if (mDelayedImeWin != null) {
- mDelayedImeWin.mWinAnimator.endDelayingAnimationStart();
+ mDelayedImeWin.endDelayingAnimationStart();
}
mDelayedImeWin = imeWin;
- imeWin.mWinAnimator.startDelayingAnimationStart();
+ imeWin.startDelayingAnimationStart();
}
// If we are already waiting for something to be drawn, clear out the old one so it
@@ -765,25 +765,27 @@ public class DockedStackDividerController {
mService.mWaitingForDrawnCallback.run();
}
mService.mWaitingForDrawnCallback = () -> {
- mAnimationStartDelayed = false;
- if (mDelayedImeWin != null) {
- mDelayedImeWin.mWinAnimator.endDelayingAnimationStart();
- }
- // If the adjust status changed since this was posted, only notify
- // the new states and don't animate.
- long duration = 0;
- if (mAdjustedForIme == adjustedForIme
- && mAdjustedForDivider == adjustedForDivider) {
- duration = IME_ADJUST_ANIM_DURATION;
- } else {
- Slog.w(TAG, "IME adjust changed while waiting for drawn:"
- + " adjustedForIme=" + adjustedForIme
- + " adjustedForDivider=" + adjustedForDivider
- + " mAdjustedForIme=" + mAdjustedForIme
- + " mAdjustedForDivider=" + mAdjustedForDivider);
+ synchronized (mService.mWindowMap) {
+ mAnimationStartDelayed = false;
+ if (mDelayedImeWin != null) {
+ mDelayedImeWin.endDelayingAnimationStart();
+ }
+ // If the adjust status changed since this was posted, only notify
+ // the new states and don't animate.
+ long duration = 0;
+ if (mAdjustedForIme == adjustedForIme
+ && mAdjustedForDivider == adjustedForDivider) {
+ duration = IME_ADJUST_ANIM_DURATION;
+ } else {
+ Slog.w(TAG, "IME adjust changed while waiting for drawn:"
+ + " adjustedForIme=" + adjustedForIme
+ + " adjustedForDivider=" + adjustedForDivider
+ + " mAdjustedForIme=" + mAdjustedForIme
+ + " mAdjustedForDivider=" + mAdjustedForDivider);
+ }
+ notifyAdjustedForImeChanged(
+ mAdjustedForIme || mAdjustedForDivider, duration);
}
- notifyAdjustedForImeChanged(
- mAdjustedForIme || mAdjustedForDivider, duration);
};
} else {
notifyAdjustedForImeChanged(
diff --git a/com/android/server/wm/DragDropController.java b/com/android/server/wm/DragDropController.java
index 65951dc6..28b4c1db 100644
--- a/com/android/server/wm/DragDropController.java
+++ b/com/android/server/wm/DragDropController.java
@@ -40,6 +40,7 @@ import android.view.View;
import com.android.internal.util.Preconditions;
import com.android.server.input.InputWindowHandle;
import com.android.server.wm.WindowManagerInternal.IDragDropCallback;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Managing drag and drop operations initiated by View#startDragAndDrop.
@@ -67,42 +68,10 @@ class DragDropController {
private final Handler mHandler;
/**
- * Lock to preserve the order of state updates.
- * The lock is used to process drag and drop state updates in order without having the window
- * manager lock.
- *
- * Suppose DragDropController invokes a callback method A, then processes the following update
- * A'. Same for a callback method B and the following update B'. The callback wants
- * DragDropController to processes the updates in the order of A' then B'.
- *
- * Without mWriteLock: the following race can happen.
- *
- * 1. Thread a calls A.
- * 2. Thread b calls B.
- * 3. Thread b acquires the window manager lock
- * 4. thread b processes the update B'
- * 5. Thread a acquires the window manager lock
- * 6. thread a processes the update A'
- *
- * With mWriteLock we can ensure the order of A' and B'
- *
- * 1. Thread a acquire mWriteLock
- * 2. Thread a calls A
- * 3. Thread a acquire the window manager lock
- * 4. Thread a processes A'
- * 5. Thread b acquire mWriteLock
- * 6. Thread b calls B
- * 7. Thread b acquire the window manager lock
- * 8. Thread b processes B'
- *
- * Don't acquire the lock while holding the window manager lock, otherwise it causes a deadlock.
- */
- private final Object mWriteLock = new Object();
-
- /**
* Callback which is used to sync drag state with the vendor-specific code.
*/
- @NonNull private IDragDropCallback mCallback = new IDragDropCallback() {};
+ @NonNull private AtomicReference<IDragDropCallback> mCallback = new AtomicReference<>(
+ new IDragDropCallback() {});
boolean dragDropActiveLocked() {
return mDragState != null;
@@ -114,9 +83,7 @@ class DragDropController {
void registerCallback(IDragDropCallback callback) {
Preconditions.checkNotNull(callback);
- synchronized (mWriteLock) {
- mCallback = callback;
- }
+ mCallback.set(callback);
}
DragDropController(WindowManagerService service, Looper looper) {
@@ -136,45 +103,48 @@ class DragDropController {
+ " asbinder=" + window.asBinder());
}
- synchronized (mWriteLock) {
- synchronized (mService.mWindowMap) {
- if (dragDropActiveLocked()) {
- Slog.w(TAG_WM, "Drag already in progress");
- return null;
- }
+ if (width <= 0 || height <= 0) {
+ Slog.w(TAG_WM, "width and height of drag shadow must be positive");
+ return null;
+ }
- // TODO(multi-display): support other displays
- final DisplayContent displayContent =
- mService.getDefaultDisplayContentLocked();
- final Display display = displayContent.getDisplay();
-
- final SurfaceControl surface = new SurfaceControl.Builder(session)
- .setName("drag surface")
- .setSize(width, height)
- .setFormat(PixelFormat.TRANSLUCENT)
- .build();
- surface.setLayerStack(display.getLayerStack());
- float alpha = 1;
- if ((flags & View.DRAG_FLAG_OPAQUE) == 0) {
- alpha = DRAG_SHADOW_ALPHA_TRANSPARENT;
- }
- surface.setAlpha(alpha);
-
- if (SHOW_TRANSACTIONS)
- Slog.i(TAG_WM, " DRAG " + surface + ": CREATE");
- outSurface.copyFrom(surface);
- final IBinder winBinder = window.asBinder();
- IBinder token = new Binder();
- mDragState = new DragState(mService, token, surface, flags, winBinder);
- mDragState.mPid = callerPid;
- mDragState.mUid = callerUid;
- mDragState.mOriginalAlpha = alpha;
- token = mDragState.mToken = new Binder();
-
- // 5 second timeout for this window to actually begin the drag
- sendTimeoutMessage(MSG_DRAG_START_TIMEOUT, winBinder);
- return token;
+ synchronized (mService.mWindowMap) {
+ if (dragDropActiveLocked()) {
+ Slog.w(TAG_WM, "Drag already in progress");
+ return null;
+ }
+
+ // TODO(multi-display): support other displays
+ final DisplayContent displayContent =
+ mService.getDefaultDisplayContentLocked();
+ final Display display = displayContent.getDisplay();
+
+ final SurfaceControl surface = new SurfaceControl.Builder(session)
+ .setName("drag surface")
+ .setSize(width, height)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .build();
+ surface.setLayerStack(display.getLayerStack());
+ float alpha = 1;
+ if ((flags & View.DRAG_FLAG_OPAQUE) == 0) {
+ alpha = DRAG_SHADOW_ALPHA_TRANSPARENT;
}
+ surface.setAlpha(alpha);
+
+ if (SHOW_TRANSACTIONS)
+ Slog.i(TAG_WM, " DRAG " + surface + ": CREATE");
+ outSurface.copyFrom(surface);
+ final IBinder winBinder = window.asBinder();
+ IBinder token = new Binder();
+ mDragState = new DragState(mService, token, surface, flags, winBinder);
+ mDragState.mPid = callerPid;
+ mDragState.mUid = callerUid;
+ mDragState.mOriginalAlpha = alpha;
+ token = mDragState.mToken = new Binder();
+
+ // 5 second timeout for this window to actually begin the drag
+ sendTimeoutMessage(MSG_DRAG_START_TIMEOUT, winBinder);
+ return token;
}
}
@@ -185,84 +155,89 @@ class DragDropController {
Slog.d(TAG_WM, "perform drag: win=" + window + " data=" + data);
}
- synchronized (mWriteLock) {
- if (!mCallback.performDrag(window, dragToken, touchSource, touchX, touchY, thumbCenterX,
- thumbCenterY, data)) {
- return false;
- }
+ final boolean callbackResult = mCallback.get().prePerformDrag(window, dragToken,
+ touchSource, touchX, touchY, thumbCenterX, thumbCenterY, data);
+ try {
synchronized (mService.mWindowMap) {
- if (mDragState == null) {
- Slog.w(TAG_WM, "No drag prepared");
- throw new IllegalStateException("performDrag() without prepareDrag()");
- }
-
- if (dragToken != mDragState.mToken) {
- Slog.w(TAG_WM, "Performing mismatched drag");
- throw new IllegalStateException("performDrag() does not match prepareDrag()");
- }
-
- final WindowState callingWin = mService.windowForClientLocked(null, window, false);
- if (callingWin == null) {
- Slog.w(TAG_WM, "Bad requesting window " + window);
- return false; // !!! TODO: throw here?
- }
+ mHandler.removeMessages(MSG_DRAG_START_TIMEOUT, window.asBinder());
+ try {
+ if (!callbackResult) {
+ return false;
+ }
- // !!! TODO: if input is not still focused on the initiating window, fail
- // the drag initiation (e.g. an alarm window popped up just as the application
- // called performDrag()
+ Preconditions.checkState(
+ mDragState != null, "performDrag() without prepareDrag()");
+ Preconditions.checkState(
+ mDragState.mToken == dragToken,
+ "performDrag() does not match prepareDrag()");
+
+ final WindowState callingWin = mService.windowForClientLocked(
+ null, window, false);
+ if (callingWin == null) {
+ Slog.w(TAG_WM, "Bad requesting window " + window);
+ return false; // !!! TODO: throw here?
+ }
- mHandler.removeMessages(MSG_DRAG_START_TIMEOUT, window.asBinder());
+ // !!! TODO: if input is not still focused on the initiating window, fail
+ // the drag initiation (e.g. an alarm window popped up just as the application
+ // called performDrag()
- // !!! TODO: extract the current touch (x, y) in screen coordinates. That
- // will let us eliminate the (touchX,touchY) parameters from the API.
+ // !!! TODO: extract the current touch (x, y) in screen coordinates. That
+ // will let us eliminate the (touchX,touchY) parameters from the API.
- // !!! FIXME: put all this heavy stuff onto the mHandler looper, as well as
- // the actual drag event dispatch stuff in the dragstate
+ // !!! FIXME: put all this heavy stuff onto the mHandler looper, as well as
+ // the actual drag event dispatch stuff in the dragstate
- final DisplayContent displayContent = callingWin.getDisplayContent();
- if (displayContent == null) {
- return false;
- }
- Display display = displayContent.getDisplay();
- mDragState.register(display);
- if (!mService.mInputManager.transferTouchFocus(callingWin.mInputChannel,
- mDragState.getInputChannel())) {
- Slog.e(TAG_WM, "Unable to transfer touch focus");
- mDragState.closeLocked();
- return false;
- }
+ final DisplayContent displayContent = callingWin.getDisplayContent();
+ if (displayContent == null) {
+ Slog.w(TAG_WM, "display content is null");
+ return false;
+ }
- mDragState.mDisplayContent = displayContent;
- mDragState.mData = data;
- mDragState.broadcastDragStartedLocked(touchX, touchY);
- mDragState.overridePointerIconLocked(touchSource);
+ final Display display = displayContent.getDisplay();
+ mDragState.register(display);
+ if (!mService.mInputManager.transferTouchFocus(callingWin.mInputChannel,
+ mDragState.getInputChannel())) {
+ Slog.e(TAG_WM, "Unable to transfer touch focus");
+ return false;
+ }
- // remember the thumb offsets for later
- mDragState.mThumbOffsetX = thumbCenterX;
- mDragState.mThumbOffsetY = thumbCenterY;
+ mDragState.mDisplayContent = displayContent;
+ mDragState.mData = data;
+ mDragState.broadcastDragStartedLocked(touchX, touchY);
+ mDragState.overridePointerIconLocked(touchSource);
+ // remember the thumb offsets for later
+ mDragState.mThumbOffsetX = thumbCenterX;
+ mDragState.mThumbOffsetY = thumbCenterY;
+
+ // Make the surface visible at the proper location
+ final SurfaceControl surfaceControl = mDragState.mSurfaceControl;
+ if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG_WM, ">>> OPEN TRANSACTION performDrag");
+ mService.openSurfaceTransaction();
+ try {
+ surfaceControl.setPosition(touchX - thumbCenterX,
+ touchY - thumbCenterY);
+ surfaceControl.setLayer(mDragState.getDragLayerLocked());
+ surfaceControl.setLayerStack(display.getLayerStack());
+ surfaceControl.show();
+ } finally {
+ mService.closeSurfaceTransaction("performDrag");
+ if (SHOW_LIGHT_TRANSACTIONS) {
+ Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag");
+ }
+ }
- // Make the surface visible at the proper location
- final SurfaceControl surfaceControl = mDragState.mSurfaceControl;
- if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG_WM, ">>> OPEN TRANSACTION performDrag");
- mService.openSurfaceTransaction();
- try {
- surfaceControl.setPosition(touchX - thumbCenterX,
- touchY - thumbCenterY);
- surfaceControl.setLayer(mDragState.getDragLayerLocked());
- surfaceControl.setLayerStack(display.getLayerStack());
- surfaceControl.show();
+ mDragState.notifyLocationLocked(touchX, touchY);
} finally {
- mService.closeSurfaceTransaction("performDrag");
- if (SHOW_LIGHT_TRANSACTIONS) {
- Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag");
+ if (mDragState != null && !mDragState.isInProgress()) {
+ mDragState.closeLocked();
}
}
-
- mDragState.notifyLocationLocked(touchX, touchY);
}
+ return true; // success!
+ } finally {
+ mCallback.get().postPerformDrag();
}
-
- return true; // success!
}
void reportDropResult(IWindow window, boolean consumed) {
@@ -271,8 +246,8 @@ class DragDropController {
Slog.d(TAG_WM, "Drop result=" + consumed + " reported by " + token);
}
- synchronized (mWriteLock) {
- mCallback.reportDropResult(window, consumed);
+ mCallback.get().preReportDropResult(window, consumed);
+ try {
synchronized (mService.mWindowMap) {
if (mDragState == null) {
// Most likely the drop recipient ANRed and we ended the drag
@@ -297,10 +272,11 @@ class DragDropController {
return; // !!! TODO: throw here?
}
-
mDragState.mDragResult = consumed;
mDragState.endDragLocked();
}
+ } finally {
+ mCallback.get().postReportDropResult();
}
}
@@ -309,8 +285,8 @@ class DragDropController {
Slog.d(TAG_WM, "cancelDragAndDrop");
}
- synchronized (mWriteLock) {
- mCallback.cancelDragAndDrop(dragToken);
+ mCallback.get().preCancelDragAndDrop(dragToken);
+ try {
synchronized (mService.mWindowMap) {
if (mDragState == null) {
Slog.w(TAG_WM, "cancelDragAndDrop() without prepareDrag()");
@@ -327,6 +303,8 @@ class DragDropController {
mDragState.mDragResult = false;
mDragState.cancelDragLocked();
}
+ } finally {
+ mCallback.get().postCancelDragAndDrop();
}
}
@@ -338,20 +316,18 @@ class DragDropController {
* @param newY Y coordinate value in dp in the screen coordinate
*/
void handleMotionEvent(boolean keepHandling, float newX, float newY) {
- synchronized (mWriteLock) {
- synchronized (mService.mWindowMap) {
- if (!dragDropActiveLocked()) {
- // The drag has ended but the clean-up message has not been processed by
- // window manager. Drop events that occur after this until window manager
- // has a chance to clean-up the input handle.
- return;
- }
+ synchronized (mService.mWindowMap) {
+ if (!dragDropActiveLocked()) {
+ // The drag has ended but the clean-up message has not been processed by
+ // window manager. Drop events that occur after this until window manager
+ // has a chance to clean-up the input handle.
+ return;
+ }
- if (keepHandling) {
- mDragState.notifyMoveLocked(newX, newY);
- } else {
- mDragState.notifyDropLocked(newX, newY);
- }
+ if (keepHandling) {
+ mDragState.notifyMoveLocked(newX, newY);
+ } else {
+ mDragState.notifyDropLocked(newX, newY);
}
}
}
@@ -414,12 +390,11 @@ class DragDropController {
if (DEBUG_DRAG) {
Slog.w(TAG_WM, "Timeout starting drag by win " + win);
}
- synchronized (mWriteLock) {
- synchronized (mService.mWindowMap) {
- // !!! TODO: ANR the app that has failed to start the drag in time
- if (mDragState != null) {
- mDragState.closeLocked();
- }
+
+ synchronized (mService.mWindowMap) {
+ // !!! TODO: ANR the app that has failed to start the drag in time
+ if (mDragState != null) {
+ mDragState.closeLocked();
}
}
break;
@@ -430,13 +405,12 @@ class DragDropController {
if (DEBUG_DRAG) {
Slog.w(TAG_WM, "Timeout ending drag to win " + win);
}
- synchronized (mWriteLock) {
- synchronized (mService.mWindowMap) {
- // !!! TODO: ANR the drag-receiving app
- if (mDragState != null) {
- mDragState.mDragResult = false;
- mDragState.endDragLocked();
- }
+
+ synchronized (mService.mWindowMap) {
+ // !!! TODO: ANR the drag-receiving app
+ if (mDragState != null) {
+ mDragState.mDragResult = false;
+ mDragState.endDragLocked();
}
}
break;
@@ -455,15 +429,13 @@ class DragDropController {
}
case MSG_ANIMATION_END: {
- synchronized (mWriteLock) {
- synchronized (mService.mWindowMap) {
- if (mDragState == null) {
- Slog.wtf(TAG_WM, "mDragState unexpectedly became null while " +
- "plyaing animation");
- return;
- }
- mDragState.closeLocked();
+ synchronized (mService.mWindowMap) {
+ if (mDragState == null) {
+ Slog.wtf(TAG_WM, "mDragState unexpectedly became null while " +
+ "plyaing animation");
+ return;
}
+ mDragState.closeLocked();
}
break;
}
diff --git a/com/android/server/wm/DragState.java b/com/android/server/wm/DragState.java
index 112e62f2..b9f437af 100644
--- a/com/android/server/wm/DragState.java
+++ b/com/android/server/wm/DragState.java
@@ -568,6 +568,14 @@ class DragState {
mToken = token;
}
+ /**
+ * Returns true if it has sent DRAG_STARTED broadcast out but has not been sent DRAG_END
+ * broadcast.
+ */
+ boolean isInProgress() {
+ return mDragInProgress;
+ }
+
private static DragEvent obtainDragEvent(WindowState win, int action,
float x, float y, Object localState,
ClipDescription description, ClipData data,
diff --git a/com/android/server/wm/InputMonitor.java b/com/android/server/wm/InputMonitor.java
index 7e29a3aa..88b7a11f 100644
--- a/com/android/server/wm/InputMonitor.java
+++ b/com/android/server/wm/InputMonitor.java
@@ -390,12 +390,13 @@ final class InputMonitor implements InputManagerService.WindowManagerCallbacks {
}
}
- final boolean inPositioning = (mService.mTaskPositioner != null);
+ final boolean inPositioning = mService.mTaskPositioningController.isPositioningLocked();
if (inPositioning) {
if (DEBUG_TASK_POSITIONING) {
Log.d(TAG_WM, "Inserting window handle for repositioning");
}
- final InputWindowHandle dragWindowHandle = mService.mTaskPositioner.mDragWindowHandle;
+ final InputWindowHandle dragWindowHandle =
+ mService.mTaskPositioningController.getDragWindowHandleLocked();
if (dragWindowHandle != null) {
addInputWindowHandle(dragWindowHandle);
} else {
diff --git a/com/android/server/wm/LocalAnimationAdapter.java b/com/android/server/wm/LocalAnimationAdapter.java
new file mode 100644
index 00000000..2173fa3a
--- /dev/null
+++ b/com/android/server/wm/LocalAnimationAdapter.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 com.android.server.wm;
+
+import android.os.SystemClock;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+/**
+ * Animation that can be executed without holding the window manager lock. See
+ * {@link SurfaceAnimationRunner}.
+ */
+class LocalAnimationAdapter implements AnimationAdapter {
+
+ private final AnimationSpec mSpec;
+
+ private final SurfaceAnimationRunner mAnimator;
+
+ LocalAnimationAdapter(AnimationSpec spec, SurfaceAnimationRunner animator) {
+ mSpec = spec;
+ mAnimator = animator;
+ }
+
+ @Override
+ public boolean getDetachWallpaper() {
+ return mSpec.getDetachWallpaper();
+ }
+
+ @Override
+ public int getBackgroundColor() {
+ return mSpec.getBackgroundColor();
+ }
+
+ @Override
+ public void startAnimation(SurfaceControl animationLeash, Transaction t,
+ OnAnimationFinishedCallback finishCallback) {
+ mAnimator.startAnimation(mSpec, animationLeash, t,
+ () -> finishCallback.onAnimationFinished(this));
+ }
+
+ @Override
+ public void onAnimationCancelled(SurfaceControl animationLeash) {
+ mAnimator.onAnimationCancelled(animationLeash);
+ }
+
+ @Override
+ public long getDurationHint() {
+ return mSpec.getDuration();
+ }
+
+ @Override
+ public long getStatusBarTransitionsStartTime() {
+ return mSpec.calculateStatusBarTransitionStartTime();
+ }
+
+ /**
+ * Describes how to apply an animation.
+ */
+ interface AnimationSpec {
+
+ /**
+ * @see AnimationAdapter#getDetachWallpaper
+ */
+ default boolean getDetachWallpaper() {
+ return false;
+ }
+
+ /**
+ * @see AnimationAdapter#getBackgroundColor
+ */
+ default int getBackgroundColor() {
+ return 0;
+ }
+
+ /**
+ * @see AnimationAdapter#getStatusBarTransitionsStartTime
+ */
+ default long calculateStatusBarTransitionStartTime() {
+ return SystemClock.uptimeMillis();
+ }
+
+ /**
+ * @return The duration of the animation.
+ */
+ long getDuration();
+
+ /**
+ * Called when the spec needs to apply the current animation state to the leash.
+ *
+ * @param t The transaction to use to apply a transform.
+ * @param leash The leash to apply the state to.
+ * @param currentPlayTime The current time of the animation.
+ */
+ void apply(Transaction t, SurfaceControl leash, long currentPlayTime);
+
+ /**
+ * @see AppTransition#canSkipFirstFrame
+ */
+ default boolean canSkipFirstFrame() {
+ return false;
+ }
+ }
+}
diff --git a/com/android/server/wm/RemoteEventTrace.java b/com/android/server/wm/RemoteEventTrace.java
index 9f65ba36..b214d35f 100644
--- a/com/android/server/wm/RemoteEventTrace.java
+++ b/com/android/server/wm/RemoteEventTrace.java
@@ -20,6 +20,7 @@ import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.DataOutputStream;
+import android.os.StrictMode;
import android.util.Slog;
import android.os.Debug;
@@ -40,22 +41,28 @@ class RemoteEventTrace {
}
void openSurfaceTransaction() {
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
mOut.writeUTF("OpenTransaction");
writeSigil();
} catch (Exception e) {
logException(e);
mService.disableSurfaceTrace();
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
}
}
void closeSurfaceTransaction() {
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
mOut.writeUTF("CloseTransaction");
writeSigil();
} catch (Exception e) {
logException(e);
mService.disableSurfaceTrace();
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
}
}
diff --git a/com/android/server/wm/RemoteSurfaceTrace.java b/com/android/server/wm/RemoteSurfaceTrace.java
index a12c2c40..d2cbf88a 100644
--- a/com/android/server/wm/RemoteSurfaceTrace.java
+++ b/com/android/server/wm/RemoteSurfaceTrace.java
@@ -20,6 +20,7 @@ import android.graphics.Rect;
import android.graphics.Region;
import android.os.IBinder;
import android.os.Parcel;
+import android.os.StrictMode;
import android.util.Slog;
import android.view.SurfaceControl;
@@ -54,67 +55,122 @@ class RemoteSurfaceTrace extends SurfaceControlWithBackground {
@Override
public void setAlpha(float alpha) {
- writeFloatEvent("Alpha", alpha);
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeFloatEvent("Alpha", alpha);
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.setAlpha(alpha);
}
@Override
public void setLayer(int zorder) {
- writeIntEvent("Layer", zorder);
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeIntEvent("Layer", zorder);
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.setLayer(zorder);
}
@Override
public void setPosition(float x, float y) {
- writeFloatEvent("Position", x, y);
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeFloatEvent("Position", x, y);
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.setPosition(x, y);
}
@Override
public void setGeometryAppliesWithResize() {
- writeEvent("GeometryAppliesWithResize");
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeEvent("GeometryAppliesWithResize");
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.setGeometryAppliesWithResize();
}
@Override
public void setSize(int w, int h) {
- writeIntEvent("Size", w, h);
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeIntEvent("Size", w, h);
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.setSize(w, h);
}
@Override
public void setWindowCrop(Rect crop) {
- writeRectEvent("Crop", crop);
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeRectEvent("Crop", crop);
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.setWindowCrop(crop);
}
@Override
public void setFinalCrop(Rect crop) {
- writeRectEvent("FinalCrop", crop);
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeRectEvent("FinalCrop", crop);
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.setFinalCrop(crop);
}
@Override
public void setLayerStack(int layerStack) {
- writeIntEvent("LayerStack", layerStack);
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeIntEvent("LayerStack", layerStack);
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.setLayerStack(layerStack);
}
@Override
public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) {
- writeFloatEvent("Matrix", dsdx, dtdx, dsdy, dtdy);
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeFloatEvent("Matrix", dsdx, dtdx, dsdy, dtdy);
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.setMatrix(dsdx, dtdx, dsdy, dtdy);
}
@Override
public void hide() {
- writeEvent("Hide");
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeEvent("Hide");
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.hide();
}
@Override
public void show() {
- writeEvent("Show");
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ writeEvent("Show");
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
super.show();
}
diff --git a/com/android/server/wm/RootWindowContainer.java b/com/android/server/wm/RootWindowContainer.java
index 4008811b..2a77c92b 100644
--- a/com/android/server/wm/RootWindowContainer.java
+++ b/com/android/server/wm/RootWindowContainer.java
@@ -101,8 +101,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> {
private static final int SET_SCREEN_BRIGHTNESS_OVERRIDE = 1;
private static final int SET_USER_ACTIVITY_TIMEOUT = 2;
- WindowManagerService mService;
-
private boolean mWallpaperForceHidingChanged = false;
private Object mLastWindowFreezeSource = null;
private Session mHoldScreen = null;
@@ -160,7 +158,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> {
};
RootWindowContainer(WindowManagerService service) {
- mService = service;
+ super(service);
mHandler = new MyHandler(service.mH.getLooper());
mWallpaperController = new WallpaperController(mService);
}
@@ -433,7 +431,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> {
if (w.mAppOp == OP_NONE) {
return;
}
- final int mode = mService.mAppOps.checkOpNoThrow(w.mAppOp, w.getOwningUid(),
+ final int mode = mService.mAppOps.noteOpNoThrow(w.mAppOp, w.getOwningUid(),
w.getOwningPackage());
w.setAppOpVisibilityLw(mode == MODE_ALLOWED || mode == MODE_DEFAULT);
}, false /* traverseTopToBottom */);
@@ -614,7 +612,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> {
defaultDisplay.pendingLayoutChanges);
}
- if (!mService.mAnimator.mAppWindowAnimating && mService.mAppTransition.isRunning()) {
+ if (!isAppAnimating() && mService.mAppTransition.isRunning()) {
// We have finished the animation of an app transition. To do this, we have delayed a
// lot of operations like showing and hiding apps, moving apps in Z-order, etc. The app
// token list reflects the correct Z-order, but the window list may now be out of sync
@@ -1037,7 +1035,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> {
final int count = mChildren.size();
for (int i = 0; i < count; ++i) {
final DisplayContent displayContent = mChildren.get(i);
- displayContent.dump(" ", pw);
+ displayContent.dump(pw, " ", true /* dumpAll */);
}
} else {
pw.println(" NO DISPLAY");
diff --git a/com/android/server/wm/Session.java b/com/android/server/wm/Session.java
index ad7c300a..192d6c84 100644
--- a/com/android/server/wm/Session.java
+++ b/com/android/server/wm/Session.java
@@ -23,9 +23,8 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG;
+
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING;
-import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -45,13 +44,13 @@ import android.os.UserHandle;
import android.util.MergedConfiguration;
import android.util.Slog;
import android.view.Display;
+import android.view.DisplayCutout;
import android.view.IWindow;
import android.view.IWindowId;
import android.view.IWindowSession;
import android.view.IWindowSessionCallback;
import android.view.InputChannel;
import android.view.Surface;
-import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.view.WindowManager;
@@ -192,15 +191,17 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
int viewVisibility, Rect outContentInsets, Rect outStableInsets,
InputChannel outInputChannel) {
return addToDisplay(window, seq, attrs, viewVisibility, Display.DEFAULT_DISPLAY,
- outContentInsets, outStableInsets, null /* outOutsets */, outInputChannel);
+ outContentInsets, outStableInsets, null /* outOutsets */, null /* cutout */,
+ outInputChannel);
}
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
- Rect outOutsets, InputChannel outInputChannel) {
+ Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout,
+ InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
- outContentInsets, outStableInsets, outOutsets, outInputChannel);
+ outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel);
}
@Override
@@ -214,7 +215,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
public int addToDisplayWithoutInputChannel(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
- outContentInsets, outStableInsets, null /* outOutsets */, null);
+ outContentInsets, outStableInsets, null /* outOutsets */, null /* cutout */, null);
}
@Override
@@ -232,6 +233,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
int requestedWidth, int requestedHeight, int viewFlags,
int flags, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
Rect outVisibleInsets, Rect outStableInsets, Rect outsets, Rect outBackdropFrame,
+ DisplayCutout.ParcelableWrapper cutout,
MergedConfiguration mergedConfiguration, Surface outSurface) {
if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from "
+ Binder.getCallingPid());
@@ -239,7 +241,8 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
int res = mService.relayoutWindow(this, window, seq, attrs,
requestedWidth, requestedHeight, viewFlags, flags,
outFrame, outOverscanInsets, outContentInsets, outVisibleInsets,
- outStableInsets, outsets, outBackdropFrame, mergedConfiguration, outSurface);
+ outStableInsets, outsets, outBackdropFrame, cutout,
+ mergedConfiguration, outSurface);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (false) Slog.d(TAG_WM, "<<<<<< EXITING relayout to "
+ Binder.getCallingPid());
@@ -365,7 +368,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
long ident = Binder.clearCallingIdentity();
try {
- return mService.startMovingTask(window, startX, startY);
+ return mService.mTaskPositioningController.startMovingTask(window, startX, startY);
} finally {
Binder.restoreCallingIdentity(ident);
}
diff --git a/com/android/server/wm/SurfaceAnimationRunner.java b/com/android/server/wm/SurfaceAnimationRunner.java
new file mode 100644
index 00000000..3a41eb0e
--- /dev/null
+++ b/com/android/server/wm/SurfaceAnimationRunner.java
@@ -0,0 +1,243 @@
+/*
+ * 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 com.android.server.wm;
+
+import static android.util.TimeUtils.NANOS_PER_MS;
+import static android.view.Choreographer.CALLBACK_TRAVERSAL;
+import static android.view.Choreographer.getSfInstance;
+
+import android.animation.AnimationHandler;
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.server.AnimationThread;
+import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
+
+/**
+ * Class to run animations without holding the window manager lock.
+ */
+class SurfaceAnimationRunner {
+
+ private final Object mLock = new Object();
+
+ /**
+ * Lock for cancelling animations. Must be acquired on it's own, or after acquiring
+ * {@link #mLock}
+ */
+ private final Object mCancelLock = new Object();
+
+ @VisibleForTesting
+ Choreographer mChoreographer;
+
+ private final Runnable mApplyTransactionRunnable = this::applyTransaction;
+ private final AnimationHandler mAnimationHandler;
+ private final Transaction mFrameTransaction;
+ private final AnimatorFactory mAnimatorFactory;
+ private boolean mApplyScheduled;
+
+ @GuardedBy("mLock")
+ @VisibleForTesting
+ final ArrayMap<SurfaceControl, RunningAnimation> mPendingAnimations = new ArrayMap<>();
+
+ @GuardedBy("mLock")
+ @VisibleForTesting
+ final ArrayMap<SurfaceControl, RunningAnimation> mRunningAnimations = new ArrayMap<>();
+
+ SurfaceAnimationRunner() {
+ this(null /* callbackProvider */, null /* animatorFactory */, new Transaction());
+ }
+
+ @VisibleForTesting
+ SurfaceAnimationRunner(@Nullable AnimationFrameCallbackProvider callbackProvider,
+ AnimatorFactory animatorFactory, Transaction frameTransaction) {
+ SurfaceAnimationThread.getHandler().runWithScissors(() -> mChoreographer = getSfInstance(),
+ 0 /* timeout */);
+ mFrameTransaction = frameTransaction;
+ mAnimationHandler = new AnimationHandler();
+ mAnimationHandler.setProvider(callbackProvider != null
+ ? callbackProvider
+ : new SfVsyncFrameCallbackProvider(mChoreographer));
+ mAnimatorFactory = animatorFactory != null
+ ? animatorFactory
+ : SfValueAnimator::new;
+ }
+
+ void startAnimation(AnimationSpec a, SurfaceControl animationLeash, Transaction t,
+ Runnable finishCallback) {
+ synchronized (mLock) {
+ final RunningAnimation runningAnim = new RunningAnimation(a, animationLeash,
+ finishCallback);
+ mPendingAnimations.put(animationLeash, runningAnim);
+ mChoreographer.postFrameCallback(this::stepAnimation);
+
+ // Some animations (e.g. move animations) require the initial transform to be applied
+ // immediately.
+ applyTransformation(runningAnim, t, 0 /* currentPlayTime */);
+ }
+ }
+
+ void onAnimationCancelled(SurfaceControl leash) {
+ synchronized (mLock) {
+ if (mPendingAnimations.containsKey(leash)) {
+ mPendingAnimations.remove(leash);
+ return;
+ }
+ final RunningAnimation anim = mRunningAnimations.get(leash);
+ if (anim != null) {
+ mRunningAnimations.remove(leash);
+ synchronized (mCancelLock) {
+ anim.mCancelled = true;
+ }
+ SurfaceAnimationThread.getHandler().post(() -> {
+ anim.mAnim.cancel();
+ applyTransaction();
+ });
+ }
+ }
+ }
+
+ private void startPendingAnimationsLocked() {
+ for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+ startAnimationLocked(mPendingAnimations.valueAt(i));
+ }
+ mPendingAnimations.clear();
+ }
+
+ private void startAnimationLocked(RunningAnimation a) {
+ final ValueAnimator anim = mAnimatorFactory.makeAnimator();
+
+ // Animation length is already expected to be scaled.
+ anim.overrideDurationScale(1.0f);
+ anim.setDuration(a.mAnimSpec.getDuration());
+ anim.addUpdateListener(animation -> {
+ synchronized (mCancelLock) {
+ if (!a.mCancelled) {
+ applyTransformation(a, mFrameTransaction, anim.getCurrentPlayTime());
+ }
+ }
+
+ // Transaction will be applied in the commit phase.
+ scheduleApplyTransaction();
+ });
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ synchronized (mCancelLock) {
+ if (!a.mCancelled) {
+ mFrameTransaction.show(a.mLeash);
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ synchronized (mLock) {
+ mRunningAnimations.remove(a.mLeash);
+ synchronized (mCancelLock) {
+ if (!a.mCancelled) {
+
+ // Post on other thread that we can push final state without jank.
+ AnimationThread.getHandler().post(a.mFinishCallback);
+ }
+ }
+ }
+ }
+ });
+ anim.start();
+ if (a.mAnimSpec.canSkipFirstFrame()) {
+ // If we can skip the first frame, we start one frame later.
+ anim.setCurrentPlayTime(mChoreographer.getFrameIntervalNanos() / NANOS_PER_MS);
+ }
+
+ // Immediately start the animation by manually applying an animation frame. Otherwise, the
+ // start time would only be set in the next frame, leading to a delay.
+ anim.doAnimationFrame(mChoreographer.getFrameTime());
+ a.mAnim = anim;
+ mRunningAnimations.put(a.mLeash, a);
+ }
+
+ private void applyTransformation(RunningAnimation a, Transaction t, long currentPlayTime) {
+ a.mAnimSpec.apply(t, a.mLeash, currentPlayTime);
+ }
+
+ private void stepAnimation(long frameTimeNanos) {
+ synchronized (mLock) {
+ startPendingAnimationsLocked();
+ }
+ }
+
+ private void scheduleApplyTransaction() {
+ if (!mApplyScheduled) {
+ mChoreographer.postCallback(CALLBACK_TRAVERSAL, mApplyTransactionRunnable,
+ null /* token */);
+ mApplyScheduled = true;
+ }
+ }
+
+ private void applyTransaction() {
+ mFrameTransaction.setAnimationTransaction();
+ mFrameTransaction.apply();
+ mApplyScheduled = false;
+ }
+
+ private static final class RunningAnimation {
+ final AnimationSpec mAnimSpec;
+ final SurfaceControl mLeash;
+ final Runnable mFinishCallback;
+ ValueAnimator mAnim;
+
+ @GuardedBy("mCancelLock")
+ private boolean mCancelled;
+
+ RunningAnimation(AnimationSpec animSpec, SurfaceControl leash, Runnable finishCallback) {
+ mAnimSpec = animSpec;
+ mLeash = leash;
+ mFinishCallback = finishCallback;
+ }
+ }
+
+ @VisibleForTesting
+ interface AnimatorFactory {
+ ValueAnimator makeAnimator();
+ }
+
+ /**
+ * Value animator that uses sf-vsync signal to tick.
+ */
+ private class SfValueAnimator extends ValueAnimator {
+
+ SfValueAnimator() {
+ setFloatValues(0f, 1f);
+ }
+
+ @Override
+ public AnimationHandler getAnimationHandler() {
+ return mAnimationHandler;
+ }
+ }
+}
diff --git a/com/android/server/wm/SurfaceAnimationThread.java b/com/android/server/wm/SurfaceAnimationThread.java
new file mode 100644
index 00000000..0d3afc06
--- /dev/null
+++ b/com/android/server/wm/SurfaceAnimationThread.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 com.android.server.wm;
+
+import static android.os.Process.THREAD_PRIORITY_DISPLAY;
+
+import android.os.Handler;
+import android.os.Trace;
+
+import com.android.server.ServiceThread;
+
+/**
+ * Thread for running {@link SurfaceAnimationRunner} that does not hold the window manager lock.
+ */
+public final class SurfaceAnimationThread extends ServiceThread {
+ private static SurfaceAnimationThread sInstance;
+ private static Handler sHandler;
+
+ private SurfaceAnimationThread() {
+ super("android.anim.lf", THREAD_PRIORITY_DISPLAY, false /*allowIo*/);
+ }
+
+ private static void ensureThreadLocked() {
+ if (sInstance == null) {
+ sInstance = new SurfaceAnimationThread();
+ sInstance.start();
+ sInstance.getLooper().setTraceTag(Trace.TRACE_TAG_WINDOW_MANAGER);
+ sHandler = new Handler(sInstance.getLooper());
+ }
+ }
+
+ public static SurfaceAnimationThread get() {
+ synchronized (SurfaceAnimationThread.class) {
+ ensureThreadLocked();
+ return sInstance;
+ }
+ }
+
+ public static Handler getHandler() {
+ synchronized (SurfaceAnimationThread.class) {
+ ensureThreadLocked();
+ return sHandler;
+ }
+ }
+}
diff --git a/com/android/server/wm/SurfaceAnimator.java b/com/android/server/wm/SurfaceAnimator.java
new file mode 100644
index 00000000..bda5bc95
--- /dev/null
+++ b/com/android/server/wm/SurfaceAnimator.java
@@ -0,0 +1,377 @@
+/*
+ * 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 com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+/**
+ * A class that can run animations on objects that have a set of child surfaces. We do this by
+ * reparenting all child surfaces of an object onto a new surface, called the "Leash". The Leash
+ * gets attached in the surface hierarchy where the the children were attached to. We then hand off
+ * the Leash to the component handling the animation, which is specified by the
+ * {@link AnimationAdapter}. When the animation is done animating, our callback to finish the
+ * animation will be invoked, at which we reparent the children back to the original parent.
+ */
+class SurfaceAnimator {
+
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "SurfaceAnimator" : TAG_WM;
+ private final WindowManagerService mService;
+ private AnimationAdapter mAnimation;
+
+ @VisibleForTesting
+ SurfaceControl mLeash;
+ private final Animatable mAnimatable;
+ private final OnAnimationFinishedCallback mInnerAnimationFinishedCallback;
+ private final Runnable mAnimationFinishedCallback;
+ private boolean mAnimationStartDelayed;
+
+ /**
+ * @param animatable The object to animate.
+ * @param animationFinishedCallback Callback to invoke when an animation has finished running.
+ */
+ SurfaceAnimator(Animatable animatable, Runnable animationFinishedCallback,
+ WindowManagerService service) {
+ mAnimatable = animatable;
+ mService = service;
+ mAnimationFinishedCallback = animationFinishedCallback;
+ mInnerAnimationFinishedCallback = getFinishedCallback(animationFinishedCallback);
+ }
+
+ private OnAnimationFinishedCallback getFinishedCallback(Runnable animationFinishedCallback) {
+ return anim -> {
+ synchronized (mService.mWindowMap) {
+ final SurfaceAnimator target = mService.mAnimationTransferMap.remove(anim);
+ if (target != null) {
+ target.mInnerAnimationFinishedCallback.onAnimationFinished(anim);
+ return;
+ }
+ if (anim != mAnimation) {
+ // Callback was from another animation - ignore.
+ return;
+ }
+
+ final Transaction t = new Transaction();
+ SurfaceControl.openTransaction();
+ try {
+ reset(t, true /* destroyLeash */);
+ animationFinishedCallback.run();
+ } finally {
+ // TODO: This should use pendingTransaction eventually, but right now things
+ // happening on the animation finished callback are happening on the global
+ // transaction.
+ SurfaceControl.mergeToGlobalTransaction(t);
+ SurfaceControl.closeTransaction();
+ }
+ }
+ };
+ }
+
+ /**
+ * Starts an animation.
+ *
+ * @param anim The object that bridges the controller, {@link SurfaceAnimator}, with the
+ * component responsible for running the animation. It runs the animation with
+ * {@link AnimationAdapter#startAnimation} once the hierarchy with
+ * the Leash has been set up.
+ * @param hidden Whether the container holding the child surfaces is currently visible or not.
+ * This is important as it will start with the leash hidden or visible before
+ * handing it to the component that is responsible to run the animation.
+ */
+ void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden) {
+ cancelAnimation(t, true /* restarting */, true /* forwardCancel */);
+ mAnimation = anim;
+ final SurfaceControl surface = mAnimatable.getSurfaceControl();
+ if (surface == null) {
+ Slog.w(TAG, "Unable to start animation, surface is null or no children.");
+ cancelAnimation();
+ return;
+ }
+ mLeash = createAnimationLeash(surface, t,
+ mAnimatable.getSurfaceWidth(), mAnimatable.getSurfaceHeight(), hidden);
+ mAnimatable.onAnimationLeashCreated(t, mLeash);
+ if (mAnimationStartDelayed) {
+ if (DEBUG_ANIM) Slog.i(TAG, "Animation start delayed");
+ return;
+ }
+ mAnimation.startAnimation(mLeash, t, mInnerAnimationFinishedCallback);
+ }
+
+ /**
+ * Begins with delaying all animations to start. Any subsequent call to {@link #startAnimation}
+ * will not start the animation until {@link #endDelayingAnimationStart} is called. When an
+ * animation start is being delayed, the animator is considered animating already.
+ */
+ void startDelayingAnimationStart() {
+
+ // We only allow delaying animation start we are not currently animating
+ if (!isAnimating()) {
+ mAnimationStartDelayed = true;
+ }
+ }
+
+ /**
+ * See {@link #startDelayingAnimationStart}.
+ */
+ void endDelayingAnimationStart() {
+ final boolean delayed = mAnimationStartDelayed;
+ mAnimationStartDelayed = false;
+ if (delayed && mAnimation != null) {
+ mAnimation.startAnimation(mLeash, mAnimatable.getPendingTransaction(),
+ mInnerAnimationFinishedCallback);
+ mAnimatable.commitPendingTransaction();
+ }
+ }
+
+ /**
+ * @return Whether we are currently running an animation, or we have a pending animation that
+ * is waiting to be started with {@link #endDelayingAnimationStart}
+ */
+ boolean isAnimating() {
+ return mAnimation != null;
+ }
+
+ /**
+ * @return The current animation spec if we are running an animation, or {@code null} otherwise.
+ */
+ AnimationAdapter getAnimation() {
+ return mAnimation;
+ }
+
+ /**
+ * Cancels any currently running animation.
+ */
+ void cancelAnimation() {
+ cancelAnimation(mAnimatable.getPendingTransaction(), false /* restarting */,
+ true /* forwardCancel */);
+ mAnimatable.commitPendingTransaction();
+ }
+
+ /**
+ * Sets the layer of the surface.
+ * <p>
+ * When the layer of the surface needs to be adjusted, we need to set it on the leash if the
+ * surface is reparented to the leash. This method takes care of that.
+ */
+ void setLayer(Transaction t, int layer) {
+ t.setLayer(mLeash != null ? mLeash : mAnimatable.getSurfaceControl(), layer);
+ }
+
+ /**
+ * Sets the surface to be relatively layered.
+ *
+ * @see #setLayer
+ */
+ void setRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) {
+ t.setRelativeLayer(mLeash != null ? mLeash : mAnimatable.getSurfaceControl(), relativeTo, layer);
+ }
+
+ /**
+ * Reparents the surface.
+ *
+ * @see #setLayer
+ */
+ void reparent(Transaction t, SurfaceControl newParent) {
+ t.reparent(mLeash != null ? mLeash : mAnimatable.getSurfaceControl(), newParent.getHandle());
+ }
+
+ /**
+ * @return True if the surface is attached to the leash; false otherwise.
+ */
+ boolean hasLeash() {
+ return mLeash != null;
+ }
+
+ void transferAnimation(SurfaceAnimator from) {
+ if (from.mLeash == null) {
+ return;
+ }
+ final SurfaceControl surface = mAnimatable.getSurfaceControl();
+ final SurfaceControl parent = mAnimatable.getParentSurfaceControl();
+ if (surface == null || parent == null) {
+ Slog.w(TAG, "Unable to transfer animation, surface or parent is null");
+ cancelAnimation();
+ return;
+ }
+ endDelayingAnimationStart();
+ final Transaction t = mAnimatable.getPendingTransaction();
+ cancelAnimation(t, true /* restarting */, true /* forwardCancel */);
+ mLeash = from.mLeash;
+ mAnimation = from.mAnimation;
+
+ // Cancel source animation, but don't let animation runner cancel the animation.
+ from.cancelAnimation(t, false /* restarting */, false /* forwardCancel */);
+ t.reparent(surface, mLeash.getHandle());
+ t.reparent(mLeash, parent.getHandle());
+ mAnimatable.onAnimationLeashCreated(t, mLeash);
+ mService.mAnimationTransferMap.put(mAnimation, this);
+ }
+
+ /**
+ * Cancels the animation, and resets the leash.
+ *
+ * @param t The transaction to use for all cancelling surface operations.
+ * @param restarting Whether we are restarting the animation.
+ * @param forwardCancel Whether to forward the cancel signal to the adapter executing the
+ * animation. This will be set to false when just transferring an animation
+ * to another animator.
+ */
+ private void cancelAnimation(Transaction t, boolean restarting, boolean forwardCancel) {
+ if (DEBUG_ANIM) Slog.i(TAG, "Cancelling animation restarting=" + restarting);
+ final SurfaceControl leash = mLeash;
+ final AnimationAdapter animation = mAnimation;
+ reset(t, forwardCancel);
+ if (animation != null) {
+ if (!mAnimationStartDelayed && forwardCancel) {
+ animation.onAnimationCancelled(leash);
+ }
+ if (!restarting) {
+ mAnimationFinishedCallback.run();
+ }
+ }
+ if (!restarting) {
+ mAnimationStartDelayed = false;
+ }
+ }
+
+ private void reset(Transaction t, boolean destroyLeash) {
+ final SurfaceControl surface = mAnimatable.getSurfaceControl();
+ final SurfaceControl parent = mAnimatable.getParentSurfaceControl();
+
+ // If the surface was destroyed, we don't care to reparent it back.
+ final boolean destroy = mLeash != null && surface != null && parent != null;
+ if (destroy) {
+ if (DEBUG_ANIM) Slog.i(TAG, "Reparenting to original parent");
+ t.reparent(surface, parent.getHandle());
+ }
+ mService.mAnimationTransferMap.remove(mAnimation);
+ if (mLeash != null && destroyLeash) {
+ mAnimatable.destroyAfterPendingTransaction(mLeash);
+ }
+ mLeash = null;
+ mAnimation = null;
+
+ // Make sure to inform the animatable after the leash was destroyed.
+ if (destroy) {
+ mAnimatable.onAnimationLeashDestroyed(t);
+ }
+ }
+
+ private SurfaceControl createAnimationLeash(SurfaceControl surface, Transaction t, int width,
+ int height, boolean hidden) {
+ if (DEBUG_ANIM) Slog.i(TAG, "Reparenting to leash");
+ final SurfaceControl.Builder builder = mAnimatable.makeAnimationLeash()
+ .setName(surface + " - animation-leash")
+ .setSize(width, height);
+ final SurfaceControl leash = builder.build();
+ if (!hidden) {
+ t.show(leash);
+ }
+ t.reparent(surface, leash.getHandle());
+ return leash;
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ pw.print(prefix); pw.print("mAnimation="); pw.print(mAnimation);
+ pw.print(" mLeash="); pw.println(mLeash);
+ }
+
+ /**
+ * Callback to be passed into {@link AnimationAdapter#startAnimation} to be invoked by the
+ * component that is running the animation when the animation is finished.
+ */
+ interface OnAnimationFinishedCallback {
+ void onAnimationFinished(AnimationAdapter anim);
+ }
+
+ /**
+ * Interface to be animated by {@link SurfaceAnimator}.
+ */
+ interface Animatable {
+
+ /**
+ * @return The pending transaction that will be committed in the next frame.
+ */
+ @NonNull Transaction getPendingTransaction();
+
+ /**
+ * Schedules a commit of the pending transaction.
+ */
+ void commitPendingTransaction();
+
+ /**
+ * Called when the was created.
+ *
+ * @param t The transaction to use to apply any necessary changes.
+ * @param leash The leash that was created.
+ */
+ void onAnimationLeashCreated(Transaction t, SurfaceControl leash);
+
+ /**
+ * Called when the leash is being destroyed, and the surface was reparented back to the
+ * original parent.
+ *
+ * @param t The transaction to use to apply any necessary changes.
+ */
+ void onAnimationLeashDestroyed(Transaction t);
+
+ /**
+ * Destroy a given surface after executing {@link #getPendingTransaction}.
+ *
+ * @see WindowContainer#destroyAfterPendingTransaction
+ */
+ void destroyAfterPendingTransaction(SurfaceControl surface);
+
+ /**
+ * @return A new surface to be used for the animation leash, inserted at the correct
+ * position in the hierarchy.
+ */
+ SurfaceControl.Builder makeAnimationLeash();
+
+ /**
+ * @return The surface of the object to be animated.
+ */
+ @Nullable SurfaceControl getSurfaceControl();
+
+ /**
+ * @return The parent of the surface object to be animated.
+ */
+ @Nullable SurfaceControl getParentSurfaceControl();
+
+ /**
+ * @return The width of the surface to be animated.
+ */
+ int getSurfaceWidth();
+
+ /**
+ * @return The height of the surface to be animated.
+ */
+ int getSurfaceHeight();
+ }
+}
diff --git a/com/android/server/wm/Task.java b/com/android/server/wm/Task.java
index 8aa129a4..3c96ca17 100644
--- a/com/android/server/wm/Task.java
+++ b/com/android/server/wm/Task.java
@@ -22,7 +22,6 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PORTRA
import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.res.Configuration.EMPTY;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static com.android.server.EventLogTags.WM_TASK_REMOVED;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STACK;
@@ -43,7 +42,6 @@ import android.graphics.Rect;
import android.util.EventLog;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
-import android.view.DisplayInfo;
import android.view.Surface;
import com.android.internal.annotations.VisibleForTesting;
@@ -59,7 +57,6 @@ class Task extends WindowContainer<AppWindowToken> {
final int mTaskId;
final int mUserId;
private boolean mDeferRemoval = false;
- final WindowManagerService mService;
final Rect mPreparedFrozenBounds = new Rect();
final Configuration mPreparedFrozenMergedConfig = new Configuration();
@@ -102,10 +99,10 @@ class Task extends WindowContainer<AppWindowToken> {
Task(int taskId, TaskStack stack, int userId, WindowManagerService service, int resizeMode,
boolean supportsPictureInPicture, TaskDescription taskDescription,
TaskWindowContainerController controller) {
+ super(service);
mTaskId = taskId;
mStack = stack;
mUserId = userId;
- mService = service;
mResizeMode = resizeMode;
mSupportsPictureInPicture = supportsPictureInPicture;
setController(controller);
@@ -163,7 +160,7 @@ class Task extends WindowContainer<AppWindowToken> {
boolean shouldDeferRemoval() {
// TODO: This should probably return false if mChildren.isEmpty() regardless if the stack
// is animating...
- return hasWindowsAlive() && mStack.isAnimating();
+ return hasWindowsAlive() && mStack.isSelfOrChildAnimating();
}
@Override
@@ -539,14 +536,7 @@ class Task extends WindowContainer<AppWindowToken> {
/** Cancels any running app transitions associated with the task. */
void cancelTaskWindowTransition() {
for (int i = mChildren.size() - 1; i >= 0; --i) {
- mChildren.get(i).mAppAnimator.clearAnimation();
- }
- }
-
- /** Cancels any running thumbnail transitions associated with the task. */
- void cancelTaskThumbnailTransition() {
- for (int i = mChildren.size() - 1; i >= 0; --i) {
- mChildren.get(i).mAppAnimator.clearThumbnail();
+ mChildren.get(i).cancelAnimation();
}
}
@@ -659,6 +649,9 @@ class Task extends WindowContainer<AppWindowToken> {
mDimmer.resetDimStates();
super.prepareSurfaces();
getDimBounds(mTmpDimBoundsRect);
+
+ // Bounds need to be relative, as the dim layer is a child.
+ mTmpDimBoundsRect.offsetTo(0, 0);
if (mDimmer.updateDims(getPendingTransaction(), mTmpDimBoundsRect)) {
scheduleAnimation();
}
@@ -680,7 +673,9 @@ class Task extends WindowContainer<AppWindowToken> {
proto.end(token);
}
- public void dump(String prefix, PrintWriter pw) {
+ @Override
+ public void dump(PrintWriter pw, String prefix, boolean dumpAll) {
+ super.dump(pw, prefix, dumpAll);
final String doublePrefix = prefix + " ";
pw.println(prefix + "taskId=" + mTaskId);
@@ -694,7 +689,7 @@ class Task extends WindowContainer<AppWindowToken> {
for (int i = mChildren.size() - 1; i >= 0; i--) {
final AppWindowToken wtoken = mChildren.get(i);
pw.println(triplePrefix + "Activity #" + i + " " + wtoken);
- wtoken.dump(pw, triplePrefix);
+ wtoken.dump(pw, triplePrefix, dumpAll);
}
}
diff --git a/com/android/server/wm/TaskPositioner.java b/com/android/server/wm/TaskPositioner.java
index 87d0a401..fa7ea2ff 100644
--- a/com/android/server/wm/TaskPositioner.java
+++ b/com/android/server/wm/TaskPositioner.java
@@ -16,15 +16,11 @@
package com.android.server.wm;
-import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT;
-import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
import static android.app.ActivityManager.RESIZE_MODE_USER;
import static android.app.ActivityManager.RESIZE_MODE_USER_FORCED;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ORIENTATION;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING;
-import static com.android.server.wm.WindowManagerDebugConfig.SHOW_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.dipToPixel;
@@ -44,7 +40,6 @@ import android.util.Slog;
import android.view.BatchedInputEventReceiver;
import android.view.Choreographer;
import android.view.Display;
-import android.view.DisplayInfo;
import android.view.InputChannel;
import android.view.InputDevice;
import android.view.InputEvent;
@@ -208,7 +203,7 @@ class TaskPositioner {
// Post back to WM to handle clean-ups. We still need the input
// event handler for the last finishInputEvent()!
- mService.mH.sendEmptyMessage(H.FINISH_TASK_POSITIONING);
+ mService.mTaskPositioningController.finishTaskPositioning();
}
handled = true;
} catch (Exception e) {
diff --git a/com/android/server/wm/TaskPositioningController.java b/com/android/server/wm/TaskPositioningController.java
new file mode 100644
index 00000000..4dfe290c
--- /dev/null
+++ b/com/android/server/wm/TaskPositioningController.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 com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.annotation.Nullable;
+import android.app.IActivityManager;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Slog;
+import android.view.Display;
+import android.view.IWindow;
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.input.InputManagerService;
+import com.android.server.input.InputWindowHandle;
+
+/**
+ * Controller for task positioning by drag.
+ */
+class TaskPositioningController {
+ private final WindowManagerService mService;
+ private final InputManagerService mInputManager;
+ private final InputMonitor mInputMonitor;
+ private final IActivityManager mActivityManager;
+ private final Handler mHandler;
+
+ @GuardedBy("WindowManagerSerivce.mWindowMap")
+ private @Nullable TaskPositioner mTaskPositioner;
+
+ boolean isPositioningLocked() {
+ return mTaskPositioner != null;
+ }
+
+ InputWindowHandle getDragWindowHandleLocked() {
+ return mTaskPositioner != null ? mTaskPositioner.mDragWindowHandle : null;
+ }
+
+ TaskPositioningController(WindowManagerService service, InputManagerService inputManager,
+ InputMonitor inputMonitor, IActivityManager activityManager, Looper looper) {
+ mService = service;
+ mInputMonitor = inputMonitor;
+ mInputManager = inputManager;
+ mActivityManager = activityManager;
+ mHandler = new Handler(looper);
+ }
+
+ boolean startMovingTask(IWindow window, float startX, float startY) {
+ WindowState win = null;
+ synchronized (mService.mWindowMap) {
+ win = mService.windowForClientLocked(null, window, false);
+ // win shouldn't be null here, pass it down to startPositioningLocked
+ // to get warning if it's null.
+ if (!startPositioningLocked(
+ win, false /*resize*/, false /*preserveOrientation*/, startX, startY)) {
+ return false;
+ }
+ }
+ try {
+ mActivityManager.setFocusedTask(win.getTask().mTaskId);
+ } catch(RemoteException e) {}
+ return true;
+ }
+
+ void handleTapOutsideTask(DisplayContent displayContent, int x, int y) {
+ mHandler.post(() -> {
+ int taskId = -1;
+ synchronized (mService.mWindowMap) {
+ final Task task = displayContent.findTaskForResizePoint(x, y);
+ if (task != null) {
+ if (!startPositioningLocked(task.getTopVisibleAppMainWindow(), true /*resize*/,
+ task.preserveOrientationOnResize(), x, y)) {
+ return;
+ }
+ taskId = task.mTaskId;
+ } else {
+ taskId = displayContent.taskIdFromPoint(x, y);
+ }
+ }
+ if (taskId >= 0) {
+ try {
+ mActivityManager.setFocusedTask(taskId);
+ } catch (RemoteException e) {
+ }
+ }
+ });
+ }
+
+ private boolean startPositioningLocked(WindowState win, boolean resize,
+ boolean preserveOrientation, float startX, float startY) {
+ if (DEBUG_TASK_POSITIONING)
+ Slog.d(TAG_WM, "startPositioningLocked: "
+ + "win=" + win + ", resize=" + resize + ", preserveOrientation="
+ + preserveOrientation + ", {" + startX + ", " + startY + "}");
+
+ if (win == null || win.getAppToken() == null) {
+ Slog.w(TAG_WM, "startPositioningLocked: Bad window " + win);
+ return false;
+ }
+ if (win.mInputChannel == null) {
+ Slog.wtf(TAG_WM, "startPositioningLocked: " + win + " has no input channel, "
+ + " probably being removed");
+ return false;
+ }
+
+ final DisplayContent displayContent = win.getDisplayContent();
+ if (displayContent == null) {
+ Slog.w(TAG_WM, "startPositioningLocked: Invalid display content " + win);
+ return false;
+ }
+
+ Display display = displayContent.getDisplay();
+ mTaskPositioner = new TaskPositioner(mService);
+ mTaskPositioner.register(displayContent);
+ mInputMonitor.updateInputWindowsLw(true /*force*/);
+
+ // We need to grab the touch focus so that the touch events during the
+ // resizing/scrolling are not sent to the app. 'win' is the main window
+ // of the app, it may not have focus since there might be other windows
+ // on top (eg. a dialog window).
+ WindowState transferFocusFromWin = win;
+ if (mService.mCurrentFocus != null && mService.mCurrentFocus != win
+ && mService.mCurrentFocus.mAppToken == win.mAppToken) {
+ transferFocusFromWin = mService.mCurrentFocus;
+ }
+ if (!mInputManager.transferTouchFocus(
+ transferFocusFromWin.mInputChannel, mTaskPositioner.mServerChannel)) {
+ Slog.e(TAG_WM, "startPositioningLocked: Unable to transfer touch focus");
+ mTaskPositioner.unregister();
+ mTaskPositioner = null;
+ mInputMonitor.updateInputWindowsLw(true /*force*/);
+ return false;
+ }
+
+ mTaskPositioner.startDrag(win, resize, preserveOrientation, startX, startY);
+ return true;
+ }
+
+ void finishTaskPositioning() {
+ mHandler.post(() -> {
+ if (DEBUG_TASK_POSITIONING) Slog.d(TAG_WM, "finishPositioning");
+
+ synchronized (mService.mWindowMap) {
+ if (mTaskPositioner != null) {
+ mTaskPositioner.unregister();
+ mTaskPositioner = null;
+ mInputMonitor.updateInputWindowsLw(true /*force*/);
+ }
+ }
+ });
+ }
+}
diff --git a/com/android/server/wm/TaskSnapshotController.java b/com/android/server/wm/TaskSnapshotController.java
index 84e475a2..f79719c2 100644
--- a/com/android/server/wm/TaskSnapshotController.java
+++ b/com/android/server/wm/TaskSnapshotController.java
@@ -93,6 +93,8 @@ class TaskSnapshotController {
private final ArraySet<Task> mTmpTasks = new ArraySet<>();
private final Handler mHandler = new Handler();
+ private final Rect mTmpRect = new Rect();
+
/**
* Flag indicating whether we are running on an Android TV device.
*/
@@ -223,11 +225,11 @@ class TaskSnapshotController {
final boolean isLowRamDevice = ActivityManager.isLowRamDeviceStatic();
final float scaleFraction = isLowRamDevice ? REDUCED_SCALE : 1f;
- final Rect taskFrame = new Rect();
- task.getBounds(taskFrame);
+ task.getBounds(mTmpRect);
+ mTmpRect.offsetTo(0, 0);
- final GraphicBuffer buffer = SurfaceControl.captureLayersToBuffer(
- task.getSurfaceControl().getHandle(), taskFrame, scaleFraction);
+ final GraphicBuffer buffer = SurfaceControl.captureLayers(
+ task.getSurfaceControl().getHandle(), mTmpRect, scaleFraction);
if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
if (DEBUG_SCREENSHOT) {
@@ -236,7 +238,7 @@ class TaskSnapshotController {
return null;
}
return new TaskSnapshot(buffer, top.getConfiguration().orientation,
- minRect(mainWindow.mContentInsets, mainWindow.mStableInsets),
+ getInsetsFromTaskBounds(mainWindow, task),
isLowRamDevice /* reduced */, scaleFraction /* scale */);
}
@@ -244,11 +246,18 @@ class TaskSnapshotController {
return mIsRunningOnWear || mIsRunningOnTv || mIsRunningOnIoT;
}
- private Rect minRect(Rect rect1, Rect rect2) {
- return new Rect(Math.min(rect1.left, rect2.left),
- Math.min(rect1.top, rect2.top),
- Math.min(rect1.right, rect2.right),
- Math.min(rect1.bottom, rect2.bottom));
+ private Rect getInsetsFromTaskBounds(WindowState state, Task task) {
+ final Rect r = new Rect();
+ r.set(state.getContentFrameLw());
+ r.intersectUnchecked(state.getStableFrameLw());
+
+ final Rect taskBounds = task.getBounds();
+
+ r.set(Math.max(0, r.left - taskBounds.left),
+ Math.max(0, r.top - taskBounds.top),
+ Math.max(0, taskBounds.right - r.right),
+ Math.max(0, taskBounds.bottom - r.bottom));
+ return r;
}
/**
diff --git a/com/android/server/wm/TaskSnapshotSurface.java b/com/android/server/wm/TaskSnapshotSurface.java
index 41915a32..259f8df1 100644
--- a/com/android/server/wm/TaskSnapshotSurface.java
+++ b/com/android/server/wm/TaskSnapshotSurface.java
@@ -57,6 +57,7 @@ import android.os.RemoteException;
import android.os.SystemClock;
import android.util.MergedConfiguration;
import android.util.Slog;
+import android.view.DisplayCutout;
import android.view.IWindowSession;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -134,6 +135,7 @@ class TaskSnapshotSurface implements StartingSurface {
window.setSession(session);
final Surface surface = new Surface();
final Rect tmpRect = new Rect();
+ final DisplayCutout.ParcelableWrapper tmpCutout = new DisplayCutout.ParcelableWrapper();
final Rect tmpFrame = new Rect();
final Rect taskBounds;
final Rect tmpContentInsets = new Rect();
@@ -195,8 +197,8 @@ class TaskSnapshotSurface implements StartingSurface {
}
try {
final int res = session.addToDisplay(window, window.mSeq, layoutParams,
- View.VISIBLE, token.getDisplayContent().getDisplayId(), tmpRect, tmpRect,
- tmpRect, null);
+ View.GONE, token.getDisplayContent().getDisplayId(), tmpRect, tmpRect,
+ tmpRect, tmpCutout, null);
if (res < 0) {
Slog.w(TAG, "Failed to add snapshot starting window res=" + res);
return null;
@@ -212,7 +214,7 @@ class TaskSnapshotSurface implements StartingSurface {
try {
session.relayout(window, window.mSeq, layoutParams, -1, -1, View.VISIBLE, 0, tmpFrame,
tmpRect, tmpContentInsets, tmpRect, tmpStableInsets, tmpRect, tmpRect,
- tmpMergedConfiguration, surface);
+ tmpCutout, tmpMergedConfiguration, surface);
} catch (RemoteException e) {
// Local call.
}
@@ -349,8 +351,9 @@ class TaskSnapshotSurface implements StartingSurface {
// Let's remove all system decorations except the status bar, but only if the task is at the
// very top of the screen.
+ final boolean isTop = mTaskBounds.top == 0 && mFrame.top == 0;
rect.inset((int) (insets.left * mSnapshot.getScale()),
- mTaskBounds.top != 0 ? (int) (insets.top * mSnapshot.getScale()) : 0,
+ isTop ? 0 : (int) (insets.top * mSnapshot.getScale()),
(int) (insets.right * mSnapshot.getScale()),
(int) (insets.bottom * mSnapshot.getScale()));
return rect;
@@ -437,7 +440,8 @@ class TaskSnapshotSurface implements StartingSurface {
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) {
if (mergedConfiguration != null && mOuter != null
&& mOuter.mOrientationOnCreation
!= mergedConfiguration.getMergedConfiguration().orientation) {
diff --git a/com/android/server/wm/TaskStack.java b/com/android/server/wm/TaskStack.java
index 4a3a3fc9..3ffc7fae 100644
--- a/com/android/server/wm/TaskStack.java
+++ b/com/android/server/wm/TaskStack.java
@@ -31,9 +31,8 @@ import static android.view.WindowManager.DOCKED_INVALID;
import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_RIGHT;
import static android.view.WindowManager.DOCKED_TOP;
-import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
+
import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_DOCKED_DIVIDER;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_MOVEMENT;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.proto.StackProto.ANIMATION_BACKGROUND_SURFACE_IS_DIMMING;
@@ -75,9 +74,6 @@ public class TaskStack extends WindowContainer<Task> implements
/** Unique identifier */
final int mStackId;
- /** The service */
- private final WindowManagerService mService;
-
/** The display this stack sits under. */
// TODO: Track parent marks like this in WindowContainer.
private DisplayContent mDisplayContent;
@@ -151,7 +147,7 @@ public class TaskStack extends WindowContainer<Task> implements
final Rect mTmpDimBoundsRect = new Rect();
TaskStack(WindowManagerService service, int stackId, StackWindowController controller) {
- mService = service;
+ super(service);
mStackId = stackId;
setController(controller);
mDockedStackMinimizeThickness = service.mContext.getResources().getDimensionPixelSize(
@@ -223,6 +219,8 @@ public class TaskStack extends WindowContainer<Task> implements
}
alignTasksToAdjustedBounds(adjusted ? mAdjustedBounds : getRawBounds(), insetBounds);
mDisplayContent.setLayoutNeeded();
+
+ updateSurfaceBounds();
}
private void alignTasksToAdjustedBounds(Rect adjustedBounds, Rect tempInsetBounds) {
@@ -244,10 +242,11 @@ public class TaskStack extends WindowContainer<Task> implements
return;
}
getRawBounds(mTmpRect);
- // TODO: Should be in relative coordinates.
- getPendingTransaction().setSize(mAnimationBackgroundSurface, mTmpRect.width(),
- mTmpRect.height()).setPosition(mAnimationBackgroundSurface, mTmpRect.left,
- mTmpRect.top);
+ final Rect stackBounds = getBounds();
+ getPendingTransaction()
+ .setSize(mAnimationBackgroundSurface, mTmpRect.width(), mTmpRect.height())
+ .setPosition(mAnimationBackgroundSurface, mTmpRect.left - stackBounds.left,
+ mTmpRect.top - stackBounds.top);
scheduleAnimation();
}
@@ -300,6 +299,7 @@ public class TaskStack extends WindowContainer<Task> implements
updateAdjustedBounds();
+ updateSurfaceBounds();
return result;
}
@@ -320,7 +320,7 @@ public class TaskStack extends WindowContainer<Task> implements
if (matchParentBounds()
|| !inSplitScreenSecondaryWindowingMode()
|| mDisplayContent == null
- || mDisplayContent.getSplitScreenPrimaryStack() != null) {
+ || mDisplayContent.getSplitScreenPrimaryStackIgnoringVisibility() != null) {
return true;
}
return false;
@@ -714,8 +714,12 @@ public class TaskStack extends WindowContainer<Task> implements
@Override
public void onConfigurationChanged(Configuration newParentConfig) {
final int prevWindowingMode = getWindowingMode();
+ // Only need to update surface size here since the super method will handle updating
+ // surface position.
+ updateSurfaceSize(getPendingTransaction());
super.onConfigurationChanged(newParentConfig);
final int windowingMode = getWindowingMode();
+
if (mDisplayContent == null || prevWindowingMode == windowingMode) {
return;
}
@@ -723,6 +727,25 @@ public class TaskStack extends WindowContainer<Task> implements
updateBoundsForWindowModeChange();
}
+ private void updateSurfaceBounds() {
+ updateSurfaceBounds(getPendingTransaction());
+ scheduleAnimation();
+ }
+
+ void updateSurfaceBounds(SurfaceControl.Transaction transaction) {
+ updateSurfaceSize(transaction);
+ updateSurfacePosition(transaction);
+ }
+
+ private void updateSurfaceSize(SurfaceControl.Transaction transaction) {
+ if (mSurfaceControl == null) {
+ return;
+ }
+
+ final Rect stackBounds = getBounds();
+ transaction.setSize(mSurfaceControl, stackBounds.width(), stackBounds.height());
+ }
+
@Override
void onDisplayChanged(DisplayContent dc) {
if (mDisplayContent != null) {
@@ -938,7 +961,7 @@ public class TaskStack extends WindowContainer<Task> implements
@Override
void removeIfPossible() {
- if (isAnimating()) {
+ if (isSelfOrChildAnimating()) {
mDeferRemoval = true;
return;
}
@@ -1117,12 +1140,12 @@ public class TaskStack extends WindowContainer<Task> implements
return false;
}
- final Rect displayContentRect = mTmpRect;
+ final Rect displayStableRect = mTmpRect;
final Rect contentBounds = mTmpRect2;
// Calculate the content bounds excluding the area occupied by IME
- getDisplayContent().getContentRect(displayContentRect);
- contentBounds.set(displayContentRect);
+ getDisplayContent().getStableRect(displayStableRect);
+ contentBounds.set(displayStableRect);
int imeTop = Math.max(imeWin.getFrameLw().top, contentBounds.top);
imeTop += imeWin.getGivenContentInsetsLw().top;
@@ -1130,7 +1153,7 @@ public class TaskStack extends WindowContainer<Task> implements
contentBounds.bottom = imeTop;
}
- final int yOffset = displayContentRect.bottom - contentBounds.bottom;
+ final int yOffset = displayStableRect.bottom - contentBounds.bottom;
final int dividerWidth =
getDisplayContent().mDividerControllerLocked.getContentWidth();
@@ -1142,7 +1165,7 @@ public class TaskStack extends WindowContainer<Task> implements
// occluded by IME. We shift its bottom up by the height of the IME, but
// leaves at least 30% of the top stack visible.
final int minTopStackBottom =
- getMinTopStackBottom(displayContentRect, getRawBounds().bottom);
+ getMinTopStackBottom(displayStableRect, getRawBounds().bottom);
final int bottom = Math.max(
getRawBounds().bottom - yOffset + dividerWidth - dividerWidthInactive,
minTopStackBottom);
@@ -1162,7 +1185,7 @@ public class TaskStack extends WindowContainer<Task> implements
final int topBeforeImeAdjust =
getRawBounds().top - dividerWidth + dividerWidthInactive;
final int minTopStackBottom =
- getMinTopStackBottom(displayContentRect,
+ getMinTopStackBottom(displayStableRect,
getRawBounds().top - dividerWidth);
final int top = Math.max(
getRawBounds().top - yOffset, minTopStackBottom + dividerWidthInactive);
@@ -1287,7 +1310,8 @@ public class TaskStack extends WindowContainer<Task> implements
proto.end(token);
}
- public void dump(String prefix, PrintWriter pw) {
+ @Override
+ void dump(PrintWriter pw, String prefix, boolean dumpAll) {
pw.println(prefix + "mStackId=" + mStackId);
pw.println(prefix + "mDeferRemoval=" + mDeferRemoval);
pw.println(prefix + "mBounds=" + getRawBounds().toShortString());
@@ -1303,7 +1327,7 @@ public class TaskStack extends WindowContainer<Task> implements
pw.println(prefix + "mAdjustedBounds=" + mAdjustedBounds.toShortString());
}
for (int taskNdx = mChildren.size() - 1; taskNdx >= 0; taskNdx--) {
- mChildren.get(taskNdx).dump(prefix + " ", pw);
+ mChildren.get(taskNdx).dump(pw, prefix + " ", dumpAll);
}
if (mAnimationBackgroundSurfaceIsShown) {
pw.println(prefix + "mWindowAnimationBackgroundSurface is shown");
@@ -1316,7 +1340,7 @@ public class TaskStack extends WindowContainer<Task> implements
pw.print(" Exiting App #"); pw.print(i);
pw.print(' '); pw.print(token);
pw.println(':');
- token.dump(pw, " ");
+ token.dump(pw, " ", dumpAll);
}
}
}
@@ -1646,7 +1670,7 @@ public class TaskStack extends WindowContainer<Task> implements
/** Returns true if a removal action is still being deferred. */
boolean checkCompleteDeferredRemoval() {
- if (isAnimating()) {
+ if (isSelfOrChildAnimating()) {
return true;
}
if (mDeferRemoval) {
@@ -1656,35 +1680,6 @@ public class TaskStack extends WindowContainer<Task> implements
return super.checkCompleteDeferredRemoval();
}
- void stepAppWindowsAnimation(long currentTime) {
- super.stepAppWindowsAnimation(currentTime);
-
- // TODO: Why aren't we just using the loop above for this? mAppAnimator.animating isn't set
- // below but is set in the loop above. See if it really matters...
-
- // Clear before using.
- mTmpAppTokens.clear();
- // We copy the list as things can be removed from the exiting token list while we are
- // processing.
- mTmpAppTokens.addAll(mExitingAppTokens);
- for (int i = 0; i < mTmpAppTokens.size(); i++) {
- final AppWindowAnimator appAnimator = mTmpAppTokens.get(i).mAppAnimator;
- appAnimator.wasAnimating = appAnimator.animating;
- if (appAnimator.stepAnimationLocked(currentTime)) {
- mService.mAnimator.setAnimating(true);
- mService.mAnimator.mAppWindowAnimating = true;
- } else if (appAnimator.wasAnimating) {
- // stopped animating, do one more pass through the layout
- appAnimator.mAppToken.setAppLayoutChanges(FINISH_LAYOUT_REDO_WALLPAPER,
- "exiting appToken " + appAnimator.mAppToken + " done");
- if (DEBUG_ANIM) Slog.v(TAG_WM,
- "updateWindowsApps...: done animating exiting " + appAnimator.mAppToken);
- }
- }
- // Clear to avoid holding reference to tokens.
- mTmpAppTokens.clear();
- }
-
@Override
int getOrientation() {
return (canSpecifyOrientation()) ? super.getOrientation() : SCREEN_ORIENTATION_UNSET;
@@ -1708,6 +1703,9 @@ public class TaskStack extends WindowContainer<Task> implements
mDimmer.resetDimStates();
super.prepareSurfaces();
getDimBounds(mTmpDimBoundsRect);
+
+ // Bounds need to be relative, as the dim layer is a child.
+ mTmpDimBoundsRect.offsetTo(0, 0);
if (mDimmer.updateDims(getPendingTransaction(), mTmpDimBoundsRect)) {
scheduleAnimation();
}
diff --git a/com/android/server/wm/TaskTapPointerEventListener.java b/com/android/server/wm/TaskTapPointerEventListener.java
index 84ad5764..5abda277 100644
--- a/com/android/server/wm/TaskTapPointerEventListener.java
+++ b/com/android/server/wm/TaskTapPointerEventListener.java
@@ -62,8 +62,8 @@ public class TaskTapPointerEventListener implements PointerEventListener {
synchronized (this) {
if (!mTouchExcludeRegion.contains(x, y)) {
- mService.mH.obtainMessage(H.TAP_OUTSIDE_TASK,
- x, y, mDisplayContent).sendToTarget();
+ mService.mTaskPositioningController.handleTapOutsideTask(
+ mDisplayContent, x, y);
}
}
}
diff --git a/com/android/server/wm/TaskWindowContainerController.java b/com/android/server/wm/TaskWindowContainerController.java
index 5caae32d..d83f28cc 100644
--- a/com/android/server/wm/TaskWindowContainerController.java
+++ b/com/android/server/wm/TaskWindowContainerController.java
@@ -199,16 +199,6 @@ public class TaskWindowContainerController
}
}
- public void cancelThumbnailTransition() {
- synchronized (mWindowMap) {
- if (mContainer == null) {
- Slog.w(TAG_WM, "cancelThumbnailTransition: taskId " + mTaskId + " not found.");
- return;
- }
- mContainer.cancelTaskThumbnailTransition();
- }
- }
-
public void setTaskDescription(TaskDescription taskDescription) {
synchronized (mWindowMap) {
if (mContainer == null) {
diff --git a/com/android/server/wm/TransactionFactory.java b/com/android/server/wm/TransactionFactory.java
new file mode 100644
index 00000000..067f0836
--- /dev/null
+++ b/com/android/server/wm/TransactionFactory.java
@@ -0,0 +1,27 @@
+/*
+ * 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 com.android.server.wm;
+
+import android.view.SurfaceControl.Transaction;
+
+/**
+ * Helper class to inject custom transaction objects into window manager.
+ */
+interface TransactionFactory {
+ Transaction make();
+};
+
diff --git a/com/android/server/wm/WallpaperController.java b/com/android/server/wm/WallpaperController.java
index 3ae45497..ac0919d9 100644
--- a/com/android/server/wm/WallpaperController.java
+++ b/com/android/server/wm/WallpaperController.java
@@ -64,8 +64,6 @@ class WallpaperController {
// to another, and this is the previous wallpaper target.
private WindowState mPrevWallpaperTarget = null;
- private int mWallpaperAnimLayerAdjustment;
-
private float mLastWallpaperX = -1;
private float mLastWallpaperY = -1;
private float mLastWallpaperXStep = -1;
@@ -112,7 +110,7 @@ class WallpaperController {
mFindResults.resetTopWallpaper = true;
if (w != winAnimator.mWindowDetachedWallpaper && w.mAppToken != null) {
// If this window's app token is hidden and not animating, it is of no interest to us.
- if (w.mAppToken.hidden && w.mAppToken.mAppAnimator.animation == null) {
+ if (w.mAppToken.isHidden() && !w.mAppToken.isSelfAnimating()) {
if (DEBUG_WALLPAPER) Slog.v(TAG,
"Skipping hidden and not animating token: " + w);
return false;
@@ -130,10 +128,10 @@ class WallpaperController {
}
final boolean keyguardGoingAwayWithWallpaper = (w.mAppToken != null
- && AppTransition.isKeyguardGoingAwayTransit(
- w.mAppToken.mAppAnimator.getTransit())
- && (w.mAppToken.mAppAnimator.getTransitFlags()
- & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER) != 0);
+ && w.mAppToken.isSelfAnimating()
+ && AppTransition.isKeyguardGoingAwayTransit(w.mAppToken.getTransit())
+ && (w.mAppToken.getTransitFlags()
+ & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER) != 0);
boolean needsShowWhenLockedWallpaper = false;
if ((w.mAttrs.flags & FLAG_SHOW_WHEN_LOCKED) != 0
@@ -204,18 +202,19 @@ class WallpaperController {
private boolean isWallpaperVisible(WindowState wallpaperTarget) {
if (DEBUG_WALLPAPER) Slog.v(TAG, "Wallpaper vis: target " + wallpaperTarget + ", obscured="
+ (wallpaperTarget != null ? Boolean.toString(wallpaperTarget.mObscured) : "??")
- + " anim=" + ((wallpaperTarget != null && wallpaperTarget.mAppToken != null)
- ? wallpaperTarget.mAppToken.mAppAnimator.animation : null)
+ + " animating=" + ((wallpaperTarget != null && wallpaperTarget.mAppToken != null)
+ ? wallpaperTarget.mAppToken.isSelfAnimating() : null)
+ " prev=" + mPrevWallpaperTarget);
return (wallpaperTarget != null
&& (!wallpaperTarget.mObscured || (wallpaperTarget.mAppToken != null
- && wallpaperTarget.mAppToken.mAppAnimator.animation != null)))
+ && wallpaperTarget.mAppToken.isSelfAnimating())))
|| mPrevWallpaperTarget != null;
}
boolean isWallpaperTargetAnimating() {
return mWallpaperTarget != null && mWallpaperTarget.mWinAnimator.isAnimationSet()
- && !mWallpaperTarget.mWinAnimator.isDummyAnimation();
+ && (mWallpaperTarget.mAppToken == null
+ || !mWallpaperTarget.mAppToken.isWaitingForTransitionStart());
}
void updateWallpaperVisibility() {
@@ -250,7 +249,7 @@ class WallpaperController {
for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) {
final WallpaperWindowToken token = mWallpaperTokens.get(i);
token.hideWallpaperToken(wasDeferred, "hideWallpapers");
- if (DEBUG_WALLPAPER_LIGHT && !token.hidden) Slog.d(TAG, "Hiding wallpaper " + token
+ if (DEBUG_WALLPAPER_LIGHT && !token.isHidden()) Slog.d(TAG, "Hiding wallpaper " + token
+ " from " + winGoingAway + " target=" + mWallpaperTarget + " prev="
+ mPrevWallpaperTarget + "\n" + Debug.getCallers(5, " "));
}
@@ -441,10 +440,6 @@ class WallpaperController {
}
}
- int getAnimLayerAdjustment() {
- return mWallpaperAnimLayerAdjustment;
- }
-
private void findWallpaperTarget(DisplayContent dc) {
mFindResults.reset();
if (dc.isStackVisible(WINDOWING_MODE_FREEFORM)) {
@@ -546,7 +541,7 @@ class WallpaperController {
private void updateWallpaperTokens(boolean visible) {
for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) {
final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx);
- token.updateWallpaperWindows(visible, mWallpaperAnimLayerAdjustment);
+ token.updateWallpaperWindows(visible);
token.getDisplayContent().assignWindowLayers(false);
}
}
@@ -565,12 +560,6 @@ class WallpaperController {
if (DEBUG_WALLPAPER) Slog.v(TAG, "Wallpaper visibility: " + visible);
if (visible) {
- // If the wallpaper target is animating, we may need to copy its layer adjustment.
- // Only do this if we are not transferring between two wallpaper targets.
- mWallpaperAnimLayerAdjustment =
- (mPrevWallpaperTarget == null && mWallpaperTarget.mAppToken != null)
- ? mWallpaperTarget.mAppToken.getAnimLayerAdjustment() : 0;
-
if (mWallpaperTarget.mWallpaperX >= 0) {
mLastWallpaperX = mWallpaperTarget.mWallpaperX;
mLastWallpaperXStep = mWallpaperTarget.mWallpaperXStep;
@@ -681,10 +670,6 @@ class WallpaperController {
pw.print("mLastWallpaperDisplayOffsetX="); pw.print(mLastWallpaperDisplayOffsetX);
pw.print(" mLastWallpaperDisplayOffsetY="); pw.println(mLastWallpaperDisplayOffsetY);
}
-
- if (mWallpaperAnimLayerAdjustment != 0) {
- pw.println(prefix + "mWallpaperAnimLayerAdjustment=" + mWallpaperAnimLayerAdjustment);
- }
}
/** Helper class for storing the results of a wallpaper target find operation. */
diff --git a/com/android/server/wm/WallpaperWindowToken.java b/com/android/server/wm/WallpaperWindowToken.java
index a12c0e50..2ae5c7bd 100644
--- a/com/android/server/wm/WallpaperWindowToken.java
+++ b/com/android/server/wm/WallpaperWindowToken.java
@@ -16,12 +16,9 @@
package com.android.server.wm;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYERS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_MOVEMENT;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -56,7 +53,7 @@ class WallpaperWindowToken extends WindowToken {
final WindowState wallpaper = mChildren.get(j);
wallpaper.hideWallpaperWindow(wasDeferred, reason);
}
- hidden = true;
+ setHidden(true);
}
void sendWindowWallpaperCommand(
@@ -92,8 +89,9 @@ class WallpaperWindowToken extends WindowToken {
final int dw = displayInfo.logicalWidth;
final int dh = displayInfo.logicalHeight;
- if (hidden == visible) {
- hidden = !visible;
+ if (isHidden() == visible) {
+ setHidden(!visible);
+
// Need to do a layout to ensure the wallpaper now has the correct size.
mDisplayContent.setLayoutNeeded();
}
@@ -115,16 +113,16 @@ class WallpaperWindowToken extends WindowToken {
void startAnimation(Animation anim) {
for (int ndx = mChildren.size() - 1; ndx >= 0; ndx--) {
final WindowState windowState = mChildren.get(ndx);
- windowState.mWinAnimator.setAnimation(anim);
+ windowState.startAnimation(anim);
}
}
- void updateWallpaperWindows(boolean visible, int animLayerAdj) {
+ void updateWallpaperWindows(boolean visible) {
- if (hidden == visible) {
+ if (isHidden() == visible) {
if (DEBUG_WALLPAPER_LIGHT) Slog.d(TAG,
"Wallpaper token " + token + " hidden=" + !visible);
- hidden = !visible;
+ setHidden(!visible);
// Need to do a layout to ensure the wallpaper now has the correct size.
mDisplayContent.setLayoutNeeded();
}
diff --git a/com/android/server/wm/WindowAnimationSpec.java b/com/android/server/wm/WindowAnimationSpec.java
new file mode 100644
index 00000000..98652934
--- /dev/null
+++ b/com/android/server/wm/WindowAnimationSpec.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 com.android.server.wm;
+
+import static com.android.server.wm.AnimationAdapter.STATUS_BAR_TRANSITION_DURATION;
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_AFTER_ANIM;
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_NONE;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.Interpolator;
+import android.view.animation.Transformation;
+import android.view.animation.TranslateAnimation;
+
+import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
+
+/**
+ * Animation spec for regular window animations.
+ */
+public class WindowAnimationSpec implements AnimationSpec {
+
+ private Animation mAnimation;
+ private final Point mPosition = new Point();
+ private final ThreadLocal<TmpValues> mThreadLocalTmps = ThreadLocal.withInitial(TmpValues::new);
+ private final boolean mCanSkipFirstFrame;
+ private final Rect mStackBounds = new Rect();
+ private int mStackClipMode;
+ private final Rect mTmpRect = new Rect();
+
+ public WindowAnimationSpec(Animation animation, Point position, boolean canSkipFirstFrame) {
+ this(animation, position, null /* stackBounds */, canSkipFirstFrame, STACK_CLIP_NONE);
+ }
+
+ public WindowAnimationSpec(Animation animation, Point position, Rect stackBounds,
+ boolean canSkipFirstFrame, int stackClipMode) {
+ mAnimation = animation;
+ if (position != null) {
+ mPosition.set(position.x, position.y);
+ }
+ mCanSkipFirstFrame = canSkipFirstFrame;
+ mStackClipMode = stackClipMode;
+ if (stackBounds != null) {
+ mStackBounds.set(stackBounds);
+ }
+ }
+
+ @Override
+ public boolean getDetachWallpaper() {
+ return mAnimation.getDetachWallpaper();
+ }
+
+ @Override
+ public int getBackgroundColor() {
+ return mAnimation.getBackgroundColor();
+ }
+
+ @Override
+ public long getDuration() {
+ return mAnimation.computeDurationHint();
+ }
+
+ @Override
+ public void apply(Transaction t, SurfaceControl leash, long currentPlayTime) {
+ final TmpValues tmp = mThreadLocalTmps.get();
+ tmp.transformation.clear();
+ mAnimation.getTransformation(currentPlayTime, tmp.transformation);
+ tmp.transformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
+ t.setMatrix(leash, tmp.transformation.getMatrix(), tmp.floats);
+ t.setAlpha(leash, tmp.transformation.getAlpha());
+ if (mStackClipMode == STACK_CLIP_NONE) {
+ t.setWindowCrop(leash, tmp.transformation.getClipRect());
+ } else if (mStackClipMode == STACK_CLIP_AFTER_ANIM) {
+ t.setFinalCrop(leash, mStackBounds);
+ t.setWindowCrop(leash, tmp.transformation.getClipRect());
+ } else {
+ mTmpRect.set(tmp.transformation.getClipRect());
+ mTmpRect.intersect(mStackBounds);
+ t.setWindowCrop(leash, mTmpRect);
+ }
+ }
+
+ @Override
+ public long calculateStatusBarTransitionStartTime() {
+ TranslateAnimation openTranslateAnimation = findTranslateAnimation(mAnimation);
+ if (openTranslateAnimation != null) {
+
+ // Some interpolators are extremely quickly mostly finished, but not completely. For
+ // our purposes, we need to find the fraction for which ther interpolator is mostly
+ // there, and use that value for the calculation.
+ float t = findAlmostThereFraction(openTranslateAnimation.getInterpolator());
+ return SystemClock.uptimeMillis()
+ + openTranslateAnimation.getStartOffset()
+ + (long)(openTranslateAnimation.getDuration() * t)
+ - STATUS_BAR_TRANSITION_DURATION;
+ } else {
+ return SystemClock.uptimeMillis();
+ }
+ }
+
+ @Override
+ public boolean canSkipFirstFrame() {
+ return mCanSkipFirstFrame;
+ }
+
+ /**
+ * Tries to find a {@link TranslateAnimation} inside the {@code animation}.
+ *
+ * @return the found animation, {@code null} otherwise
+ */
+ private static TranslateAnimation findTranslateAnimation(Animation animation) {
+ if (animation instanceof TranslateAnimation) {
+ return (TranslateAnimation) animation;
+ } else if (animation instanceof AnimationSet) {
+ AnimationSet set = (AnimationSet) animation;
+ for (int i = 0; i < set.getAnimations().size(); i++) {
+ Animation a = set.getAnimations().get(i);
+ if (a instanceof TranslateAnimation) {
+ return (TranslateAnimation) a;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Binary searches for a {@code t} such that there exists a {@code -0.01 < eps < 0.01} for which
+ * {@code interpolator(t + eps) > 0.99}.
+ */
+ private static float findAlmostThereFraction(Interpolator interpolator) {
+ float val = 0.5f;
+ float adj = 0.25f;
+ while (adj >= 0.01f) {
+ if (interpolator.getInterpolation(val) < 0.99f) {
+ val += adj;
+ } else {
+ val -= adj;
+ }
+ adj /= 2;
+ }
+ return val;
+ }
+
+ private static class TmpValues {
+ final Transformation transformation = new Transformation();
+ final float[] floats = new float[9];
+ }
+}
diff --git a/com/android/server/wm/WindowAnimator.java b/com/android/server/wm/WindowAnimator.java
index 7c56f00b..8bceb640 100644
--- a/com/android/server/wm/WindowAnimator.java
+++ b/com/android/server/wm/WindowAnimator.java
@@ -50,16 +50,14 @@ public class WindowAnimator {
/** Is any window animating? */
private boolean mAnimating;
- private boolean mLastAnimating;
-
- /** Is any app window animating? */
- boolean mAppWindowAnimating;
+ private boolean mLastRootAnimating;
final Choreographer.FrameCallback mAnimationFrameCallback;
/** Time of current animation step. Reset on each iteration */
long mCurrentTime;
+ boolean mAppWindowAnimating;
/** Skip repeated AppWindowTokens initialization. Note that AppWindowsToken's version of this
* is a long initialized to Long.MIN_VALUE so that it doesn't match this value on startup. */
int mAnimTransactionSequence;
@@ -142,21 +140,10 @@ public class WindowAnimator {
scheduleAnimation();
}
- // Simulate back-pressure by opening and closing an empty animation transaction. This makes
- // sure that an animation frame is at least presented once on the screen. We do this outside
- // of the regular transaction such that we can avoid holding the window manager lock in case
- // we receive back-pressure from SurfaceFlinger. Since closing an animation transaction
- // without the window manager locks leads to ordering issues (as the transaction will be
- // processed only at the beginning of the next frame which may result in another transaction
- // that was executed later in WM side gets executed first on SF side), we don't update any
- // Surface properties here such that reordering doesn't cause issues.
- mService.executeEmptyAnimationTransaction();
-
synchronized (mService.mWindowMap) {
mCurrentTime = frameTimeNs / TimeUtils.NANOS_PER_MS;
mBulkUpdateParams = SET_ORIENTATION_CHANGE_COMPLETE;
mAnimating = false;
- mAppWindowAnimating = false;
if (DEBUG_WINDOW_TRACE) {
Slog.i(TAG, "!!! animate: entry time=" + mCurrentTime);
}
@@ -170,7 +157,6 @@ public class WindowAnimator {
for (int i = 0; i < numDisplays; i++) {
final int displayId = mDisplayContentsAnimators.keyAt(i);
final DisplayContent dc = mService.mRoot.getDisplayContentOrCreate(displayId);
- dc.stepAppWindowsAnimation(mCurrentTime);
DisplayContentsAnimator displayAnimator = mDisplayContentsAnimators.valueAt(i);
final ScreenRotationAnimation screenRotationAnimation =
@@ -251,7 +237,8 @@ public class WindowAnimator {
mWindowPlacerLocked.requestTraversal();
}
- if (mAnimating && !mLastAnimating) {
+ final boolean rootAnimating = mService.mRoot.isSelfOrChildAnimating();
+ if (rootAnimating && !mLastRootAnimating) {
// Usually app transitions but quite a load onto the system already (with all the
// things happening in app), so pause task snapshot persisting to not increase the
@@ -259,13 +246,13 @@ public class WindowAnimator {
mService.mTaskSnapshotController.setPersisterPaused(true);
Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "animating", 0);
}
- if (!mAnimating && mLastAnimating) {
+ if (!rootAnimating && mLastRootAnimating) {
mWindowPlacerLocked.requestTraversal();
mService.mTaskSnapshotController.setPersisterPaused(false);
Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER, "animating", 0);
}
- mLastAnimating = mAnimating;
+ mLastRootAnimating = rootAnimating;
if (mRemoveReplacedWindows) {
mService.mRoot.removeReplacedWindows();
diff --git a/com/android/server/wm/WindowContainer.java b/com/android/server/wm/WindowContainer.java
index 64675825..b251b53b 100644
--- a/com/android/server/wm/WindowContainer.java
+++ b/com/android/server/wm/WindowContainer.java
@@ -19,20 +19,31 @@ package com.android.server.wm;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.proto.WindowContainerProto.CONFIGURATION_CONTAINER;
import static com.android.server.wm.proto.WindowContainerProto.ORIENTATION;
+import static com.android.server.wm.proto.WindowContainerProto.VISIBLE;
import static android.view.SurfaceControl.Transaction;
import android.annotation.CallSuper;
import android.content.res.Configuration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.Slog;
import android.view.MagnificationSpec;
import android.view.SurfaceControl;
+import android.view.SurfaceControl.Builder;
import android.view.SurfaceSession;
import android.util.Pools;
import android.util.proto.ProtoOutputStream;
+
import com.android.internal.util.ToBooleanFunction;
+import com.android.server.wm.SurfaceAnimator.Animatable;
+import java.io.PrintWriter;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.function.Consumer;
@@ -45,7 +56,9 @@ import java.util.function.Predicate;
* changes are made to this class.
*/
class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<E>
- implements Comparable<WindowContainer> {
+ implements Comparable<WindowContainer>, Animatable {
+
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowContainer" : TAG_WM;
static final int POSITION_TOP = Integer.MAX_VALUE;
static final int POSITION_BOTTOM = Integer.MIN_VALUE;
@@ -55,7 +68,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
* For removing or setting new parent {@link #setParent} should be used, because it also
* performs configuration updates based on new parent's settings.
*/
- private WindowContainer mParent = null;
+ private WindowContainer<WindowContainer> mParent = null;
// List of children for this window container. List is in z-order as the children appear on
// screen with the top-most window container at the tail of the list.
@@ -68,21 +81,35 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
new Pools.SynchronizedPool<>(3);
// The owner/creator for this container. No controller if null.
- WindowContainerController mController;
+ WindowContainerController mController;
protected SurfaceControl mSurfaceControl;
+ private int mLastLayer = 0;
+ private SurfaceControl mLastRelativeToLayer = null;
/**
* Applied as part of the animation pass in "prepareSurfaces".
*/
- private Transaction mPendingTransaction = new Transaction();
+ protected final Transaction mPendingTransaction;
+ protected final SurfaceAnimator mSurfaceAnimator;
+ protected final WindowManagerService mService;
+
+ private final Point mTmpPos = new Point();
+
+ /** Total number of elements in this subtree, including our own hierarchy element. */
+ private int mTreeWeight = 1;
+
+ WindowContainer(WindowManagerService service) {
+ mService = service;
+ mPendingTransaction = service.mTransactionFactory.make();
+ mSurfaceAnimator = new SurfaceAnimator(this, this::onAnimationFinished, service);
+ }
@Override
final protected WindowContainer getParent() {
return mParent;
}
-
@Override
protected int getChildCount() {
return mChildren.size();
@@ -93,7 +120,14 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
return mChildren.get(index);
}
- final protected void setParent(WindowContainer parent) {
+ @Override
+ public void onConfigurationChanged(Configuration newParentConfig) {
+ super.onConfigurationChanged(newParentConfig);
+ updateSurfacePosition(getPendingTransaction());
+ scheduleAnimation();
+ }
+
+ final protected void setParent(WindowContainer<WindowContainer> parent) {
mParent = parent;
// Removing parent usually means that we've detached this entity to destroy it or to attach
// to another parent. In both cases we don't need to update the configuration now.
@@ -115,14 +149,18 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
if (mParent == null) {
return;
}
+
if (mSurfaceControl == null) {
// If we don't yet have a surface, but we now have a parent, we should
// build a surface.
mSurfaceControl = makeSurface().build();
getPendingTransaction().show(mSurfaceControl);
} else {
- // If we have a surface but a new parent, we just need to perform a reparent.
- getPendingTransaction().reparent(mSurfaceControl, mParent.mSurfaceControl.getHandle());
+ // If we have a surface but a new parent, we just need to perform a reparent. Go through
+ // surface animator such that hierarchy is preserved when animating, i.e.
+ // mSurfaceControl stays attached to the leash and we just reparent the leash to the
+ // new parent.
+ reparentSurfaceControl(getPendingTransaction(), mParent.mSurfaceControl);
}
// Either way we need to ask the parent to assign us a Z-order.
@@ -131,8 +169,8 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
// Temp. holders for a chain of containers we are currently processing.
- private final LinkedList<WindowContainer> mTmpChain1 = new LinkedList();
- private final LinkedList<WindowContainer> mTmpChain2 = new LinkedList();
+ private final LinkedList<WindowContainer> mTmpChain1 = new LinkedList<>();
+ private final LinkedList<WindowContainer> mTmpChain2 = new LinkedList<>();
/**
* Adds the input window container has a child of this container in order based on the input
@@ -165,6 +203,8 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
} else {
mChildren.add(positionToAdd, child);
}
+ onChildAdded(child);
+
// Set the parent after we've actually added a child in case a subclass depends on this.
child.setParent(this);
}
@@ -178,10 +218,21 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
+ " can't add to container=" + getName());
}
mChildren.add(index, child);
+ onChildAdded(child);
+
// Set the parent after we've actually added a child in case a subclass depends on this.
child.setParent(this);
}
+ private void onChildAdded(WindowContainer child) {
+ mTreeWeight += child.mTreeWeight;
+ WindowContainer parent = getParent();
+ while (parent != null) {
+ parent.mTreeWeight += child.mTreeWeight;
+ parent = parent.getParent();
+ }
+ }
+
/**
* Removes the input child container from this container which is its parent.
*
@@ -190,6 +241,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
@CallSuper
void removeChild(E child) {
if (mChildren.remove(child)) {
+ onChildRemoved(child);
child.setParent(null);
} else {
throw new IllegalArgumentException("removeChild: container=" + child.getName()
@@ -197,6 +249,15 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
}
+ private void onChildRemoved(WindowContainer child) {
+ mTreeWeight -= child.mTreeWeight;
+ WindowContainer parent = getParent();
+ while (parent != null) {
+ parent.mTreeWeight -= child.mTreeWeight;
+ parent = parent.getParent();
+ }
+ }
+
/**
* Removes this window container and its children with no regard for what else might be going on
* in the system. For example, the container will be removed during animation if this method is
@@ -206,12 +267,14 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
@CallSuper
void removeImmediately() {
while (!mChildren.isEmpty()) {
- final WindowContainer child = mChildren.peekLast();
+ final E child = mChildren.peekLast();
child.removeImmediately();
// Need to do this after calling remove on the child because the child might try to
// remove/detach itself from its parent which will cause an exception if we remove
// it before calling remove on the child.
- mChildren.remove(child);
+ if (mChildren.remove(child)) {
+ onChildRemoved(child);
+ }
}
if (mSurfaceControl != null) {
@@ -230,6 +293,34 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
/**
+ * @return The index of this element in the hierarchy tree in prefix order.
+ */
+ int getPrefixOrderIndex() {
+ if (mParent == null) {
+ return 0;
+ }
+ return mParent.getPrefixOrderIndex(this);
+ }
+
+ private int getPrefixOrderIndex(WindowContainer child) {
+ int order = 0;
+ for (int i = 0; i < mChildren.size(); i++) {
+ final WindowContainer childI = mChildren.get(i);
+ if (child == childI) {
+ break;
+ }
+ order += childI.mTreeWeight;
+ }
+ if (mParent != null) {
+ order += mParent.getPrefixOrderIndex(this);
+ }
+
+ // We also need to count ourselves.
+ order++;
+ return order;
+ }
+
+ /**
* Removes this window container and its children taking care not to remove them during a
* critical stage in the system. For example, some containers will not be removed during
* animation if this method is called.
@@ -393,16 +484,54 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
}
+ /**
+ * @return Whether our own container is running an animation or any child, no matter how deep in
+ * the hierarchy, is animating.
+ */
+ boolean isSelfOrChildAnimating() {
+ if (isSelfAnimating()) {
+ return true;
+ }
+ for (int j = mChildren.size() - 1; j >= 0; j--) {
+ final WindowContainer wc = mChildren.get(j);
+ if (wc.isSelfOrChildAnimating()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return Whether our own container is running an animation or our parent is animating. This
+ * doesn't consider whether children are animating.
+ */
boolean isAnimating() {
+
+ // We are animating if we ourselves are animating or if our parent is animating.
+ return isSelfAnimating() || mParent != null && mParent.isAnimating();
+ }
+
+ /**
+ * @return {@code true} if in this subtree of the hierarchy we have an {@link AppWindowToken}
+ * that is {@link #isSelfAnimating}; {@code false} otherwise.
+ */
+ boolean isAppAnimating() {
for (int j = mChildren.size() - 1; j >= 0; j--) {
final WindowContainer wc = mChildren.get(j);
- if (wc.isAnimating()) {
+ if (wc.isAppAnimating()) {
return true;
}
}
return false;
}
+ /**
+ * @return Whether our own container running an animation at the moment.
+ */
+ boolean isSelfAnimating() {
+ return mSurfaceAnimator.isAnimating();
+ }
+
void sendAppVisibilityToClients() {
for (int i = mChildren.size() - 1; i >= 0; --i) {
final WindowContainer wc = mChildren.get(i);
@@ -483,14 +612,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
}
- /** Step currently ongoing animation for App window containers. */
- void stepAppWindowsAnimation(long currentTime) {
- for (int i = mChildren.size() - 1; i >= 0; --i) {
- final WindowContainer wc = mChildren.get(i);
- wc.stepAppWindowsAnimation(currentTime);
- }
- }
-
void onAppTransitionDone() {
for (int i = mChildren.size() - 1; i >= 0; --i) {
final WindowContainer wc = mChildren.get(i);
@@ -735,6 +856,15 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
.setParent(mSurfaceControl);
}
+ @Override
+ public SurfaceControl getParentSurfaceControl() {
+ final WindowContainer parent = getParent();
+ if (parent == null) {
+ return null;
+ }
+ return parent.getSurfaceControl();
+ }
+
/**
* @return Whether this WindowContainer should be magnified by the accessibility magnifier.
*/
@@ -755,34 +885,64 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
void assignLayer(Transaction t, int layer) {
- if (mSurfaceControl != null) {
- t.setLayer(mSurfaceControl, layer);
+ final boolean changed = layer != mLastLayer || mLastRelativeToLayer != null;
+ if (mSurfaceControl != null && changed) {
+ setLayer(t, layer);
+ mLastLayer = layer;
+ mLastRelativeToLayer = null;
+ }
+ }
+
+ void assignRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) {
+ final boolean changed = layer != mLastLayer || mLastRelativeToLayer != relativeTo;
+ if (mSurfaceControl != null && changed) {
+ setRelativeLayer(t, relativeTo, layer);
+ mLastLayer = layer;
+ mLastRelativeToLayer = relativeTo;
}
}
+ protected void setLayer(Transaction t, int layer) {
+
+ // Route through surface animator to accommodate that our surface control might be
+ // attached to the leash, and leash is attached to parent container.
+ mSurfaceAnimator.setLayer(t, layer);
+ }
+
+ protected void setRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) {
+
+ // Route through surface animator to accommodate that our surface control might be
+ // attached to the leash, and leash is attached to parent container.
+ mSurfaceAnimator.setRelativeLayer(t, relativeTo, layer);
+ }
+
+ protected void reparentSurfaceControl(Transaction t, SurfaceControl newParent) {
+ mSurfaceAnimator.reparent(t, newParent);
+ }
+
void assignChildLayers(Transaction t) {
int layer = 0;
- boolean boosting = false;
// We use two passes as a way to promote children which
// need Z-boosting to the end of the list.
- for (int i = 0; i < 2; i++ ) {
- for (int j = 0; j < mChildren.size(); ++j) {
- final WindowContainer wc = mChildren.get(j);
- if (wc.needsZBoost() && !boosting) {
- continue;
- }
- wc.assignLayer(t, layer);
- wc.assignChildLayers(t);
-
- layer++;
+ for (int j = 0; j < mChildren.size(); ++j) {
+ final WindowContainer wc = mChildren.get(j);
+ wc.assignChildLayers(t);
+ if (!wc.needsZBoost()) {
+ wc.assignLayer(t, layer++);
+ }
+ }
+ for (int j = 0; j < mChildren.size(); ++j) {
+ final WindowContainer wc = mChildren.get(j);
+ if (wc.needsZBoost()) {
+ wc.assignLayer(t, layer++);
}
- boosting = true;
}
}
void assignChildLayers() {
assignChildLayers(getPendingTransaction());
+ scheduleAnimation();
}
boolean needsZBoost() {
@@ -809,6 +969,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
final long token = proto.start(fieldId);
super.writeToProto(proto, CONFIGURATION_CONTAINER, trim);
proto.write(ORIENTATION, mOrientation);
+ proto.write(VISIBLE, isVisible());
proto.end(token);
}
@@ -876,7 +1037,8 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
}
- SurfaceControl getSurfaceControl() {
+ @Override
+ public SurfaceControl getSurfaceControl() {
return mSurfaceControl;
}
@@ -886,13 +1048,144 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
* rather than an intentional design, so please take care when
* expanding use.
*/
- void destroyAfterPendingTransaction(SurfaceControl surface) {
+ @Override
+ public void destroyAfterPendingTransaction(SurfaceControl surface) {
if (mParent != null) {
mParent.destroyAfterPendingTransaction(surface);
}
}
-
- Transaction getPendingTransaction() {
+
+ @Override
+ public Transaction getPendingTransaction() {
return mPendingTransaction;
}
+
+ /**
+ * Starts an animation on the container.
+ *
+ * @param anim The animation to run.
+ * @param hidden Whether our container is currently hidden. TODO This should use isVisible at
+ * some point but the meaning is too weird to work for all containers.
+ */
+ void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden) {
+ if (DEBUG_ANIM) Slog.v(TAG, "Starting animation on " + this + ": " + anim);
+
+ // TODO: This should use isVisible() but because isVisible has a really weird meaning at
+ // the moment this doesn't work for all animatable window containers.
+ mSurfaceAnimator.startAnimation(t, anim, hidden);
+ }
+
+ void transferAnimation(WindowContainer from) {
+ mSurfaceAnimator.transferAnimation(from.mSurfaceAnimator);
+ }
+
+ void cancelAnimation() {
+ mSurfaceAnimator.cancelAnimation();
+ }
+
+ @Override
+ public Builder makeAnimationLeash() {
+ return makeSurface();
+ }
+
+ /**
+ * @return The layer on which all app animations are happening.
+ */
+ SurfaceControl getAppAnimationLayer() {
+ final WindowContainer parent = getParent();
+ if (parent != null) {
+ return parent.getAppAnimationLayer();
+ }
+ return null;
+ }
+
+ @Override
+ public void commitPendingTransaction() {
+ scheduleAnimation();
+ }
+
+ private void reassignLayer(Transaction t) {
+ final WindowContainer parent = getParent();
+ if (parent != null) {
+ parent.assignChildLayers(t);
+ }
+ }
+
+ @Override
+ public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
+ reassignLayer(t);
+ }
+
+ @Override
+ public void onAnimationLeashDestroyed(Transaction t) {
+ reassignLayer(t);
+ }
+
+ /**
+ * Called when an animation has finished running.
+ */
+ protected void onAnimationFinished() {
+ }
+
+ /**
+ * @return The currently running animation, if any, or {@code null} otherwise.
+ */
+ AnimationAdapter getAnimation() {
+ return mSurfaceAnimator.getAnimation();
+ }
+
+ /**
+ * @see SurfaceAnimator#startDelayingAnimationStart
+ */
+ void startDelayingAnimationStart() {
+ mSurfaceAnimator.startDelayingAnimationStart();
+ }
+
+ /**
+ * @see SurfaceAnimator#endDelayingAnimationStart
+ */
+ void endDelayingAnimationStart() {
+ mSurfaceAnimator.endDelayingAnimationStart();
+ }
+
+ @Override
+ public int getSurfaceWidth() {
+ return mSurfaceControl.getWidth();
+ }
+
+ @Override
+ public int getSurfaceHeight() {
+ return mSurfaceControl.getHeight();
+ }
+
+ @CallSuper
+ void dump(PrintWriter pw, String prefix, boolean dumpAll) {
+ if (mSurfaceAnimator.isAnimating()) {
+ pw.print(prefix); pw.println("ContainerAnimator:");
+ mSurfaceAnimator.dump(pw, prefix + " ");
+ }
+ }
+
+ void updateSurfacePosition(SurfaceControl.Transaction transaction) {
+ if (mSurfaceControl == null) {
+ return;
+ }
+
+ getRelativePosition(mTmpPos);
+ transaction.setPosition(mSurfaceControl, mTmpPos.x, mTmpPos.y);
+
+ for (int i = mChildren.size() - 1; i >= 0; i--) {
+ mChildren.get(i).updateSurfacePosition(transaction);
+ }
+ }
+
+ void getRelativePosition(Point outPos) {
+ final Rect bounds = getBounds();
+ outPos.set(bounds.left, bounds.top);
+ final WindowContainer parent = getParent();
+ if (parent != null) {
+ final Rect parentBounds = parent.getBounds();
+ outPos.offset(-parentBounds.left, -parentBounds.top);
+ }
+ }
}
diff --git a/com/android/server/wm/WindowManagerInternal.java b/com/android/server/wm/WindowManagerInternal.java
index 036f7b0a..1935a44c 100644
--- a/com/android/server/wm/WindowManagerInternal.java
+++ b/com/android/server/wm/WindowManagerInternal.java
@@ -118,8 +118,11 @@ public abstract class WindowManagerInternal {
* of AppTransition.TRANSIT_* values
* @param openToken the token for the opening app
* @param closeToken the token for the closing app
- * @param openAnimation the animation for the opening app
- * @param closeAnimation the animation for the closing app
+ * @param duration the total duration of the transition
+ * @param statusBarAnimationStartTime the desired start time for all visual animations in
+ * the status bar caused by this app transition in uptime millis
+ * @param statusBarAnimationDuration the duration for all visual animations in the status
+ * bar caused by this app transition in millis
*
* @return Return any bit set of {@link WindowManagerPolicy#FINISH_LAYOUT_REDO_LAYOUT},
* {@link WindowManagerPolicy#FINISH_LAYOUT_REDO_CONFIG},
@@ -127,7 +130,7 @@ public abstract class WindowManagerInternal {
* or {@link WindowManagerPolicy#FINISH_LAYOUT_REDO_ANIM}.
*/
public int onAppTransitionStartingLocked(int transit, IBinder openToken, IBinder closeToken,
- Animation openAnimation, Animation closeAnimation) {
+ long duration, long statusBarAnimationStartTime, long statusBarAnimationDuration) {
return 0;
}
@@ -151,23 +154,38 @@ public abstract class WindowManagerInternal {
*/
public interface IDragDropCallback {
/**
- * Called when drag operation is started.
+ * Called when drag operation is starting.
*/
- default boolean performDrag(IWindow window, IBinder dragToken,
+ default boolean prePerformDrag(IWindow window, IBinder dragToken,
int touchSource, float touchX, float touchY, float thumbCenterX, float thumbCenterY,
ClipData data) {
return true;
}
/**
- * Called when drop result is reported.
+ * Called when drag operation is started.
+ */
+ default void postPerformDrag() {}
+
+ /**
+ * Called when drop result is being reported.
+ */
+ default void preReportDropResult(IWindow window, boolean consumed) {}
+
+ /**
+ * Called when drop result was reported.
+ */
+ default void postReportDropResult() {}
+
+ /**
+ * Called when drag operation is being cancelled.
*/
- default void reportDropResult(IWindow window, boolean consumed) {}
+ default void preCancelDragAndDrop(IBinder dragToken) {}
/**
- * Called when drag operation is cancelled.
+ * Called when drag operation was cancelled.
*/
- default void cancelDragAndDrop(IBinder dragToken) {}
+ default void postCancelDragAndDrop() {}
}
/**
diff --git a/com/android/server/wm/WindowManagerService.java b/com/android/server/wm/WindowManagerService.java
index 82f842c0..0a2ffbc9 100644
--- a/com/android/server/wm/WindowManagerService.java
+++ b/com/android/server/wm/WindowManagerService.java
@@ -65,17 +65,15 @@ import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static android.view.WindowManagerGlobal.RELAYOUT_DEFER_SURFACE_DESTROY;
import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED;
+
+import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN;
import static com.android.server.LockGuard.INDEX_WINDOW;
import static com.android.server.LockGuard.installLock;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
-import static com.android.server.wm.AppTransition.TRANSIT_UNSET;
-import static com.android.server.wm.AppWindowAnimator.PROLONG_ANIMATION_AT_END;
-import static com.android.server.wm.AppWindowAnimator.PROLONG_ANIMATION_AT_START;
import static com.android.server.wm.KeyguardDisableHandler.KEYGUARD_POLICY_CHANGED;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_BOOT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_CONFIGURATION;
@@ -175,6 +173,7 @@ import android.os.UserHandle;
import android.os.WorkSource;
import android.provider.Settings;
import android.text.format.DateUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.EventLog;
@@ -189,6 +188,7 @@ import android.util.TypedValue;
import android.util.proto.ProtoOutputStream;
import android.view.AppTransitionAnimationSpec;
import android.view.Display;
+import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.IAppTransitionAnimationSpecsFuture;
@@ -212,6 +212,7 @@ import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.Surface;
import android.view.SurfaceControl;
+import android.view.SurfaceControl.Builder;
import android.view.SurfaceSession;
import android.view.View;
import android.view.WindowContentFrameStats;
@@ -229,6 +230,7 @@ import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.internal.policy.IShortcutService;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastPrintWriter;
+import com.android.internal.util.LatencyTracker;
import com.android.internal.view.IInputContext;
import com.android.internal.view.IInputMethodClient;
import com.android.internal.view.IInputMethodManager;
@@ -561,12 +563,10 @@ public class WindowManagerService extends IWindowManager.Stub
int mDockedStackCreateMode = SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
Rect mDockedStackCreateBounds;
- private final SparseIntArray mTmpTaskIds = new SparseIntArray();
-
boolean mForceResizableTasks = false;
boolean mSupportsPictureInPicture = false;
- private boolean mDisableTransitionAnimation = false;
+ boolean mDisableTransitionAnimation = false;
int getDragLayerLocked() {
return mPolicy.getWindowLayerFromTypeLw(TYPE_DRAG) * TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
@@ -720,8 +720,6 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
- boolean mAnimateWallpaperWithTarget;
-
// TODO: Move to RootWindowContainer
AppWindowToken mFocusedApp = null;
@@ -750,7 +748,7 @@ public class WindowManagerService extends IWindowManager.Stub
// Whether or not a layout can cause a wake up when theater mode is enabled.
boolean mAllowTheaterModeWakeFromLayout;
- TaskPositioner mTaskPositioner;
+ final TaskPositioningController mTaskPositioningController;
final DragDropController mDragDropController;
// For frozen screen animations.
@@ -764,13 +762,21 @@ public class WindowManagerService extends IWindowManager.Stub
int mTransactionSequence;
final WindowAnimator mAnimator;
+ final SurfaceAnimationRunner mSurfaceAnimationRunner;
+ /**
+ * Keeps track of which animations got transferred to which animators. Entries will get cleaned
+ * up when the animation finishes.
+ */
+ final ArrayMap<AnimationAdapter, SurfaceAnimator> mAnimationTransferMap = new ArrayMap<>();
final BoundsAnimationController mBoundsAnimationController;
private final PointerEventDispatcher mPointerEventDispatcher;
private WindowContentFrameStats mTempWindowRenderStats;
+ private final LatencyTracker mLatencyTracker;
+
/**
* Whether the UI is currently running in touch mode (not showing
* navigational focus because the user is directly pressing the screen).
@@ -800,12 +806,8 @@ public class WindowManagerService extends IWindowManager.Stub
static WindowManagerThreadPriorityBooster sThreadPriorityBooster =
new WindowManagerThreadPriorityBooster();
- class DefaultSurfaceBuilderFactory implements SurfaceBuilderFactory {
- public SurfaceControl.Builder make(SurfaceSession s) {
- return new SurfaceControl.Builder(s);
- }
- };
- SurfaceBuilderFactory mSurfaceBuilderFactory = new DefaultSurfaceBuilderFactory();
+ SurfaceBuilderFactory mSurfaceBuilderFactory = SurfaceControl.Builder::new;
+ TransactionFactory mTransactionFactory = SurfaceControl.Transaction::new;
static void boostPriorityForLockedSection() {
sThreadPriorityBooster.boost();
@@ -850,35 +852,6 @@ public class WindowManagerService extends IWindowManager.Stub
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
}
-
- /**
- * Executes an empty animation transaction without holding the WM lock to simulate
- * back-pressure. See {@link WindowAnimator#animate} why this is needed.
- */
- void executeEmptyAnimationTransaction() {
- try {
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "openSurfaceTransaction");
- synchronized (mWindowMap) {
- if (mRoot.mSurfaceTraceEnabled) {
- mRoot.mRemoteEventTrace.openSurfaceTransaction();
- }
- SurfaceControl.openTransaction();
- SurfaceControl.setAnimationTransaction();
- if (mRoot.mSurfaceTraceEnabled) {
- mRoot.mRemoteEventTrace.closeSurfaceTransaction();
- }
- }
- } finally {
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- }
- try {
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "closeSurfaceTransaction");
- SurfaceControl.closeTransaction();
- } finally {
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- }
- }
-
/** Listener to notify activity manager about app transitions. */
final WindowManagerInternal.AppTransitionListener mActivityManagerAppTransitionNotifier
= new WindowManagerInternal.AppTransitionListener() {
@@ -1070,6 +1043,8 @@ public class WindowManagerService extends IWindowManager.Stub
filter.addAction(Intent.ACTION_USER_REMOVED);
mContext.registerReceiver(mBroadcastReceiver, filter);
+ mLatencyTracker = LatencyTracker.getInstance(context);
+
mSettingsObserver = new SettingsObserver();
mHoldingScreenWakeLock = mPowerManager.newWakeLock(
@@ -1077,10 +1052,13 @@ public class WindowManagerService extends IWindowManager.Stub
mHoldingScreenWakeLock.setReferenceCounted(false);
mAnimator = new WindowAnimator(this);
+ mSurfaceAnimationRunner = new SurfaceAnimationRunner();
mAllowTheaterModeWakeFromLayout = context.getResources().getBoolean(
com.android.internal.R.bool.config_allowTheaterModeWakeFromWindowLayout);
+ mTaskPositioningController = new TaskPositioningController(
+ this, mInputManager, mInputMonitor, mActivityManager, mH.getLooper());
mDragDropController = new DragDropController(this, mH.getLooper());
LocalServices.addService(WindowManagerInternal.class, new LocalService());
@@ -1129,9 +1107,9 @@ public class WindowManagerService extends IWindowManager.Stub
}
public int addWindow(Session session, IWindow client, int seq,
- WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
+ LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
- InputChannel outInputChannel) {
+ DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel) {
int[] appOp = new int[1];
int res = mPolicy.checkAddPermission(attrs, appOp);
if (res != WindowManagerGlobal.ADD_OKAY) {
@@ -1466,7 +1444,7 @@ public class WindowManagerService extends IWindowManager.Stub
taskBounds = null;
}
if (mPolicy.getInsetHintLw(win.mAttrs, taskBounds, displayFrames, outContentInsets,
- outStableInsets, outOutsets)) {
+ outStableInsets, outOutsets, outDisplayCutout)) {
res |= WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_NAV_BAR;
}
@@ -1494,7 +1472,7 @@ public class WindowManagerService extends IWindowManager.Stub
// Don't do layout here, the window must call
// relayout to be displayed, so we'll do it there.
- displayContent.assignWindowLayers(false /* setLayoutNeeded */);
+ win.getParent().assignChildLayers();
if (focusChanged) {
mInputMonitor.setInputFocusLw(mCurrentFocus, false /*updateInputWindows*/);
@@ -1846,11 +1824,12 @@ public class WindowManagerService extends IWindowManager.Stub
}
public int relayoutWindow(Session session, IWindow client, int seq,
- WindowManager.LayoutParams attrs, int requestedWidth,
+ LayoutParams attrs, int requestedWidth,
int requestedHeight, int viewVisibility, int flags,
Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
Rect outVisibleInsets, Rect outStableInsets, Rect outOutsets, Rect outBackdropFrame,
- MergedConfiguration mergedConfiguration, Surface outSurface) {
+ DisplayCutout.ParcelableWrapper outCutout, MergedConfiguration mergedConfiguration,
+ Surface outSurface) {
int result = 0;
boolean configChanged;
final boolean hasStatusBarPermission =
@@ -1963,6 +1942,13 @@ public class WindowManagerService extends IWindowManager.Stub
+ " newVis=" + viewVisibility, stack);
}
+ win.setDisplayLayoutNeeded();
+ win.mGivenInsetsPending = (flags & WindowManagerGlobal.RELAYOUT_INSETS_PENDING) != 0;
+
+ // We may be deferring layout passes at the moment, but since the client is interested
+ // in the new out values right now we need to force a layout.
+ mWindowPlacerLocked.performSurfacePlacement(true /* force */);
+
// We should only relayout if the view is visible, it is a starting window, or the
// associated appToken is not hidden.
final boolean shouldRelayout = viewVisibility == View.VISIBLE &&
@@ -1972,15 +1958,6 @@ public class WindowManagerService extends IWindowManager.Stub
if (shouldRelayout) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "relayoutWindow: viewVisibility_1");
- // We are about to create a surface, but we didn't run a layout yet. So better run
- // a layout now that we already know the right size, as a resize call will make the
- // surface transaction blocking until next vsync and slow us down.
- // TODO: Ideally we'd create the surface after running layout a bit further down,
- // but moving this seems to be too risky at this point in the release.
- if (win.mLayoutSeq == -1) {
- win.setDisplayLayoutNeeded();
- mWindowPlacerLocked.performSurfacePlacement(true);
- }
result = win.relayoutVisibleWindow(result, attrChanges, oldVisibility);
try {
@@ -2082,16 +2059,11 @@ public class WindowManagerService extends IWindowManager.Stub
mUnknownAppVisibilityController.notifyRelayouted(win.mAppToken);
}
- win.setDisplayLayoutNeeded();
- win.mGivenInsetsPending = (flags & WindowManagerGlobal.RELAYOUT_INSETS_PENDING) != 0;
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
"relayoutWindow: updateOrientationFromAppTokens");
configChanged = updateOrientationFromAppTokensLocked(false, displayId);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- // We may be deferring layout passes at the moment, but since the client is interested
- // in the new out values right now we need to force a layout.
- mWindowPlacerLocked.performSurfacePlacement(true /* force */);
if (toBeDisplayed && win.mIsWallpaper) {
DisplayInfo displayInfo = win.getDisplayContent().getDisplayInfo();
dc.mWallpaperController.updateWallpaperOffset(
@@ -2135,6 +2107,7 @@ public class WindowManagerService extends IWindowManager.Stub
win.mLastRelayoutContentInsets.set(win.mContentInsets);
outVisibleInsets.set(win.mVisibleInsets);
outStableInsets.set(win.mStableInsets);
+ outCutout.set(win.mDisplayCutout);
outOutsets.set(win.mOutsets);
outBackdropFrame.set(win.getBackdropFrame(win.mFrame));
if (localLOGV) Slog.v(
@@ -2178,18 +2151,15 @@ public class WindowManagerService extends IWindowManager.Stub
if (win.isWinVisibleLw() && winAnimator.applyAnimationLocked(transit, false)) {
focusMayChange = isDefaultDisplay;
win.mAnimatingExit = true;
- win.mWinAnimator.mAnimating = true;
} else if (win.mWinAnimator.isAnimationSet()) {
// Currently in a hide animation... turn this into
// an exit.
win.mAnimatingExit = true;
- win.mWinAnimator.mAnimating = true;
} else if (win.getDisplayContent().mWallpaperController.isWallpaperTarget(win)) {
// If the wallpaper is currently behind this
// window, we need to change both of them inside
// of a transaction to avoid artifacts.
win.mAnimatingExit = true;
- win.mWinAnimator.mAnimating = true;
} else {
if (mInputMethodWindow == win) {
setInputMethodWindowLocked(null);
@@ -2280,83 +2250,6 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
- boolean applyAnimationLocked(AppWindowToken atoken, WindowManager.LayoutParams lp,
- int transit, boolean enter, boolean isVoiceInteraction) {
- if (mDisableTransitionAnimation) {
- if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) {
- Slog.v(TAG_WM,
- "applyAnimation: transition animation is disabled. atoken=" + atoken);
- }
- atoken.mAppAnimator.clearAnimation();
- return false;
- }
- // Only apply an animation if the display isn't frozen. If it is
- // frozen, there is no reason to animate and it can cause strange
- // artifacts when we unfreeze the display if some different animation
- // is running.
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WM#applyAnimationLocked");
- if (atoken.okToAnimate()) {
- final DisplayContent displayContent = atoken.getTask().getDisplayContent();
- final DisplayInfo displayInfo = displayContent.getDisplayInfo();
- final int width = displayInfo.appWidth;
- final int height = displayInfo.appHeight;
- if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.v(TAG_WM,
- "applyAnimation: atoken=" + atoken);
-
- // Determine the visible rect to calculate the thumbnail clip
- final WindowState win = atoken.findMainWindow();
- final Rect frame = new Rect(0, 0, width, height);
- final Rect displayFrame = new Rect(0, 0,
- displayInfo.logicalWidth, displayInfo.logicalHeight);
- final Rect insets = new Rect();
- final Rect stableInsets = new Rect();
- Rect surfaceInsets = null;
- final boolean freeform = win != null && win.inFreeformWindowingMode();
- if (win != null) {
- // Containing frame will usually cover the whole screen, including dialog windows.
- // For freeform workspace windows it will not cover the whole screen and it also
- // won't exactly match the final freeform window frame (e.g. when overlapping with
- // the status bar). In that case we need to use the final frame.
- if (freeform) {
- frame.set(win.mFrame);
- } else {
- frame.set(win.mContainingFrame);
- }
- surfaceInsets = win.getAttrs().surfaceInsets;
- insets.set(win.mContentInsets);
- stableInsets.set(win.mStableInsets);
- }
-
- if (atoken.mLaunchTaskBehind) {
- // Differentiate the two animations. This one which is briefly on the screen
- // gets the !enter animation, and the other activity which remains on the
- // screen gets the enter animation. Both appear in the mOpeningApps set.
- enter = false;
- }
- if (DEBUG_APP_TRANSITIONS) Slog.d(TAG_WM, "Loading animation for app transition."
- + " transit=" + AppTransition.appTransitionToString(transit) + " enter=" + enter
- + " frame=" + frame + " insets=" + insets + " surfaceInsets=" + surfaceInsets);
- final Configuration displayConfig = displayContent.getConfiguration();
- Animation a = mAppTransition.loadAnimation(lp, transit, enter, displayConfig.uiMode,
- displayConfig.orientation, frame, displayFrame, insets, surfaceInsets,
- stableInsets, isVoiceInteraction, freeform, atoken.getTask().mTaskId);
- if (a != null) {
- if (DEBUG_ANIM) logWithStack(TAG, "Loaded animation " + a + " for " + atoken);
- final int containingWidth = frame.width();
- final int containingHeight = frame.height();
- atoken.mAppAnimator.setAnimation(a, containingWidth, containingHeight, width,
- height, mAppTransition.canSkipFirstFrame(),
- mAppTransition.getAppStackClipMode(),
- transit, mAppTransition.getTransitFlags());
- }
- } else {
- atoken.mAppAnimator.clearAnimation();
- }
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-
- return atoken.mAppAnimator.animation != null;
- }
-
boolean checkCallingPermission(String permission, String func) {
// Quick check: if the calling permission is me, it's all okay.
if (Binder.getCallingPid() == myPid()) {
@@ -2702,29 +2595,13 @@ public class WindowManagerService extends IWindowManager.Stub
synchronized (mWindowMap) {
mAppTransition.overridePendingAppTransitionMultiThumb(specs, onAnimationStartedCallback,
onAnimationFinishedCallback, scaleUp);
- prolongAnimationsFromSpecs(specs, scaleUp);
}
}
- void prolongAnimationsFromSpecs(@NonNull AppTransitionAnimationSpec[] specs, boolean scaleUp) {
- // This is used by freeform <-> recents windows transition. We need to synchronize
- // the animation with the appearance of the content of recents, so we will make
- // animation stay on the first or last frame a little longer.
- mTmpTaskIds.clear();
- for (int i = specs.length - 1; i >= 0; i--) {
- mTmpTaskIds.put(specs[i].taskId, 0);
- }
- for (final WindowState win : mWindowMap.values()) {
- final Task task = win.getTask();
- if (task != null && mTmpTaskIds.get(task.mTaskId, -1) != -1
- && task.inFreeformWindowingMode()) {
- final AppWindowToken appToken = win.mAppToken;
- if (appToken != null && appToken.mAppAnimator != null) {
- appToken.mAppAnimator.startProlongAnimation(scaleUp ?
- PROLONG_ANIMATION_AT_START : PROLONG_ANIMATION_AT_END);
- }
- }
+ public void overridePendingAppTransitionStartCrossProfileApps() {
+ synchronized (mWindowMap) {
+ mAppTransition.overridePendingAppTransitionStartCrossProfileApps();
}
}
@@ -2750,8 +2627,8 @@ public class WindowManagerService extends IWindowManager.Stub
synchronized (mWindowMap) {
for (final WindowState win : mWindowMap.values()) {
final AppWindowToken appToken = win.mAppToken;
- if (appToken != null && appToken.mAppAnimator != null) {
- appToken.mAppAnimator.endProlongedAnimation();
+ if (appToken != null) {
+ appToken.endDelayingAnimationStart();
}
}
mAppTransition.notifyProlongedAnimationsEnded();
@@ -2806,15 +2683,6 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
- void updateTokenInPlaceLocked(AppWindowToken wtoken, int transit) {
- if (transit != TRANSIT_UNSET) {
- if (wtoken.mAppAnimator.animation == AppWindowAnimator.sDummyAnimation) {
- wtoken.mAppAnimator.setNullAnimation();
- }
- applyAnimationLocked(wtoken, null, transit, false, false);
- }
- }
-
public void setDockedStackCreateState(int mode, Rect bounds) {
synchronized (mWindowMap) {
setDockedStackCreateStateLocked(mode, bounds);
@@ -3046,11 +2914,6 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
- public boolean inKeyguardRestrictedInputMode() {
- return mPolicy.inKeyguardRestrictedKeyInputMode();
- }
-
- @Override
public boolean isKeyguardLocked() {
return mPolicy.isKeyguardLocked();
}
@@ -4445,107 +4308,6 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
- boolean startMovingTask(IWindow window, float startX, float startY) {
- WindowState win = null;
- synchronized (mWindowMap) {
- win = windowForClientLocked(null, window, false);
- // win shouldn't be null here, pass it down to startPositioningLocked
- // to get warning if it's null.
- if (!startPositioningLocked(
- win, false /*resize*/, false /*preserveOrientation*/, startX, startY)) {
- return false;
- }
- }
- try {
- mActivityManager.setFocusedTask(win.getTask().mTaskId);
- } catch(RemoteException e) {}
- return true;
- }
-
- private void handleTapOutsideTask(DisplayContent displayContent, int x, int y) {
- int taskId = -1;
- synchronized (mWindowMap) {
- final Task task = displayContent.findTaskForResizePoint(x, y);
- if (task != null) {
- if (!startPositioningLocked(task.getTopVisibleAppMainWindow(), true /*resize*/,
- task.preserveOrientationOnResize(), x, y)) {
- return;
- }
- taskId = task.mTaskId;
- } else {
- taskId = displayContent.taskIdFromPoint(x, y);
- }
- }
- if (taskId >= 0) {
- try {
- mActivityManager.setFocusedTask(taskId);
- } catch(RemoteException e) {}
- }
- }
-
- private boolean startPositioningLocked(WindowState win, boolean resize,
- boolean preserveOrientation, float startX, float startY) {
- if (DEBUG_TASK_POSITIONING)
- Slog.d(TAG_WM, "startPositioningLocked: "
- + "win=" + win + ", resize=" + resize + ", preserveOrientation="
- + preserveOrientation + ", {" + startX + ", " + startY + "}");
-
- if (win == null || win.getAppToken() == null) {
- Slog.w(TAG_WM, "startPositioningLocked: Bad window " + win);
- return false;
- }
- if (win.mInputChannel == null) {
- Slog.wtf(TAG_WM, "startPositioningLocked: " + win + " has no input channel, "
- + " probably being removed");
- return false;
- }
-
- final DisplayContent displayContent = win.getDisplayContent();
- if (displayContent == null) {
- Slog.w(TAG_WM, "startPositioningLocked: Invalid display content " + win);
- return false;
- }
-
- Display display = displayContent.getDisplay();
- mTaskPositioner = new TaskPositioner(this);
- mTaskPositioner.register(displayContent);
- mInputMonitor.updateInputWindowsLw(true /*force*/);
-
- // We need to grab the touch focus so that the touch events during the
- // resizing/scrolling are not sent to the app. 'win' is the main window
- // of the app, it may not have focus since there might be other windows
- // on top (eg. a dialog window).
- WindowState transferFocusFromWin = win;
- if (mCurrentFocus != null && mCurrentFocus != win
- && mCurrentFocus.mAppToken == win.mAppToken) {
- transferFocusFromWin = mCurrentFocus;
- }
- if (!mInputManager.transferTouchFocus(
- transferFocusFromWin.mInputChannel, mTaskPositioner.mServerChannel)) {
- Slog.e(TAG_WM, "startPositioningLocked: Unable to transfer touch focus");
- mTaskPositioner.unregister();
- mTaskPositioner = null;
- mInputMonitor.updateInputWindowsLw(true /*force*/);
- return false;
- }
-
- mTaskPositioner.startDrag(win, resize, preserveOrientation, startX, startY);
- return true;
- }
-
- private void finishPositioning() {
- if (DEBUG_TASK_POSITIONING) {
- Slog.d(TAG_WM, "finishPositioning");
- }
- synchronized (mWindowMap) {
- if (mTaskPositioner != null) {
- mTaskPositioner.unregister();
- mTaskPositioner = null;
- mInputMonitor.updateInputWindowsLw(true /*force*/);
- }
- }
- }
-
// -------------------------------------------------------------
// Input Events and Focus Management
// -------------------------------------------------------------
@@ -4668,6 +4430,7 @@ public class WindowManagerService extends IWindowManager.Stub
if (displayContent != null) {
mAnimator.addDisplayLocked(displayId);
displayContent.initializeDisplayBaseInfo();
+ reconfigureDisplayLocked(displayContent);
}
}
}
@@ -4715,7 +4478,6 @@ public class WindowManagerService extends IWindowManager.Stub
public static final int DO_ANIMATION_CALLBACK = 26;
public static final int CLIENT_FREEZE_TIMEOUT = 30;
- public static final int TAP_OUTSIDE_TASK = 31;
public static final int NOTIFY_ACTIVITY_DRAWN = 32;
public static final int ALL_WINDOWS_DRAWN = 33;
@@ -4729,8 +4491,6 @@ public class WindowManagerService extends IWindowManager.Stub
public static final int RESET_ANR_MESSAGE = 38;
public static final int WALLPAPER_DRAW_PENDING_TIMEOUT = 39;
- public static final int FINISH_TASK_POSITIONING = 40;
-
public static final int UPDATE_DOCKED_STACK_DIVIDER = 41;
public static final int WINDOW_REPLACEMENT_TIMEOUT = 46;
@@ -5007,16 +4767,6 @@ public class WindowManagerService extends IWindowManager.Stub
break;
}
- case TAP_OUTSIDE_TASK: {
- handleTapOutsideTask((DisplayContent)msg.obj, msg.arg1, msg.arg2);
- }
- break;
-
- case FINISH_TASK_POSITIONING: {
- finishPositioning();
- }
- break;
-
case NOTIFY_ACTIVITY_DRAWN:
try {
mActivityManager.notifyActivityDrawn((IBinder) msg.obj);
@@ -5727,7 +5477,9 @@ public class WindowManagerService extends IWindowManager.Stub
/** Note that Locked in this case is on mLayoutToAnim */
void scheduleAnimationLocked() {
- mAnimator.scheduleAnimation();
+ if (mAnimator != null) {
+ mAnimator.scheduleAnimation();
+ }
}
// TODO: Move to DisplayContent
@@ -5863,6 +5615,7 @@ public class WindowManagerService extends IWindowManager.Stub
Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024);
}
+ mLatencyTracker.onActionStart(ACTION_ROTATE_SCREEN);
// TODO(multidisplay): rotation on non-default displays
if (CUSTOM_SCREEN_ROTATION && displayContent.isDefaultDisplay) {
mExitAnimId = exitAnim;
@@ -5987,6 +5740,7 @@ public class WindowManagerService extends IWindowManager.Stub
if (configChanged) {
mH.obtainMessage(H.SEND_NEW_CONFIGURATION, displayId).sendToTarget();
}
+ mLatencyTracker.onActionEnd(ACTION_ROTATE_SCREEN);
}
static int getPropertyInt(String[] tokens, int index, int defUnits, int defDps,
@@ -6804,7 +6558,6 @@ public class WindowManagerService extends IWindowManager.Stub
synchronized (mWindowMap) {
final Display display = mDisplayManager.getDisplay(displayId);
if (display != null) {
- createDisplayContentLocked(display);
displayReady(displayId);
}
mWindowPlacerLocked.requestTraversal();
diff --git a/com/android/server/wm/WindowManagerShellCommand.java b/com/android/server/wm/WindowManagerShellCommand.java
index 23586955..b9dc9db7 100644
--- a/com/android/server/wm/WindowManagerShellCommand.java
+++ b/com/android/server/wm/WindowManagerShellCommand.java
@@ -18,9 +18,23 @@ package com.android.server.wm;
import static android.os.Build.IS_USER;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
import android.os.ShellCommand;
+import android.os.UserHandle;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.IWindowManager;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.PrintWriter;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* ShellCommands for WindowManagerService.
@@ -29,33 +43,279 @@ import java.io.PrintWriter;
*/
public class WindowManagerShellCommand extends ShellCommand {
- private final WindowManagerService mService;
+ // IPC interface to activity manager -- don't need to do additional security checks.
+ private final IWindowManager mInterface;
+
+ // Internal service impl -- must perform security checks before touching.
+ private final WindowManagerService mInternal;
public WindowManagerShellCommand(WindowManagerService service) {
- mService = service;
+ mInterface = service;
+ mInternal = service;
}
@Override
public int onCommand(String cmd) {
- switch (cmd) {
- case "tracing":
- return mService.mWindowTracing.onShellCommand(this, getNextArgRequired());
- default:
- return handleDefaultCommands(cmd);
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ switch (cmd) {
+ case "size":
+ return runDisplaySize(pw);
+ case "density":
+ return runDisplayDensity(pw);
+ case "overscan":
+ return runDisplayOverscan(pw);
+ case "scaling":
+ return runDisplayScaling(pw);
+ case "screen-capture":
+ return runSetScreenCapture(pw);
+ case "dismiss-keyguard":
+ return runDismissKeyguard(pw);
+ case "surface-trace":
+ return runSurfaceTrace(pw);
+ case "tracing":
+ // XXX this should probably be changed to use openFileForSystem() to create
+ // the output trace file, so the shell gets the correct semantics for where
+ // trace files can be written.
+ return mInternal.mWindowTracing.onShellCommand(this,
+ getNextArgRequired());
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ } catch (RemoteException e) {
+ pw.println("Remote exception: " + e);
+ }
+ return -1;
+ }
+
+ private int runDisplaySize(PrintWriter pw) throws RemoteException {
+ String size = getNextArg();
+ int w, h;
+ if (size == null) {
+ Point initialSize = new Point();
+ Point baseSize = new Point();
+ try {
+ mInterface.getInitialDisplaySize(Display.DEFAULT_DISPLAY, initialSize);
+ mInterface.getBaseDisplaySize(Display.DEFAULT_DISPLAY, baseSize);
+ pw.println("Physical size: " + initialSize.x + "x" + initialSize.y);
+ if (!initialSize.equals(baseSize)) {
+ pw.println("Override size: " + baseSize.x + "x" + baseSize.y);
+ }
+ } catch (RemoteException e) {
+ }
+ return 0;
+ } else if ("reset".equals(size)) {
+ w = h = -1;
+ } else {
+ int div = size.indexOf('x');
+ if (div <= 0 || div >= (size.length()-1)) {
+ getErrPrintWriter().println("Error: bad size " + size);
+ return -1;
+ }
+ String wstr = size.substring(0, div);
+ String hstr = size.substring(div+1);
+ try {
+ w = parseDimension(wstr);
+ h = parseDimension(hstr);
+ } catch (NumberFormatException e) {
+ getErrPrintWriter().println("Error: bad number " + e);
+ return -1;
+ }
+ }
+
+ if (w >= 0 && h >= 0) {
+ // TODO(multidisplay): For now Configuration only applies to main screen.
+ mInterface.setForcedDisplaySize(Display.DEFAULT_DISPLAY, w, h);
+ } else {
+ mInterface.clearForcedDisplaySize(Display.DEFAULT_DISPLAY);
+ }
+ return 0;
+ }
+
+ private int runDisplayDensity(PrintWriter pw) throws RemoteException {
+ String densityStr = getNextArg();
+ int density;
+ if (densityStr == null) {
+ try {
+ int initialDensity = mInterface.getInitialDisplayDensity(Display.DEFAULT_DISPLAY);
+ int baseDensity = mInterface.getBaseDisplayDensity(Display.DEFAULT_DISPLAY);
+ pw.println("Physical density: " + initialDensity);
+ if (initialDensity != baseDensity) {
+ pw.println("Override density: " + baseDensity);
+ }
+ } catch (RemoteException e) {
+ }
+ return 0;
+ } else if ("reset".equals(densityStr)) {
+ density = -1;
+ } else {
+ try {
+ density = Integer.parseInt(densityStr);
+ } catch (NumberFormatException e) {
+ getErrPrintWriter().println("Error: bad number " + e);
+ return -1;
+ }
+ if (density < 72) {
+ getErrPrintWriter().println("Error: density must be >= 72");
+ return -1;
+ }
+ }
+
+ if (density > 0) {
+ // TODO(multidisplay): For now Configuration only applies to main screen.
+ mInterface.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, density,
+ UserHandle.USER_CURRENT);
+ } else {
+ mInterface.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY,
+ UserHandle.USER_CURRENT);
+ }
+ return 0;
+ }
+
+ private int runDisplayOverscan(PrintWriter pw) throws RemoteException {
+ String overscanStr = getNextArgRequired();
+ Rect rect = new Rect();
+ if ("reset".equals(overscanStr)) {
+ rect.set(0, 0, 0, 0);
+ } else {
+ final Pattern FLATTENED_PATTERN = Pattern.compile(
+ "(-?\\d+),(-?\\d+),(-?\\d+),(-?\\d+)");
+ Matcher matcher = FLATTENED_PATTERN.matcher(overscanStr);
+ if (!matcher.matches()) {
+ getErrPrintWriter().println("Error: bad rectangle arg: " + overscanStr);
+ return -1;
+ }
+ rect.left = Integer.parseInt(matcher.group(1));
+ rect.top = Integer.parseInt(matcher.group(2));
+ rect.right = Integer.parseInt(matcher.group(3));
+ rect.bottom = Integer.parseInt(matcher.group(4));
+ }
+
+ mInterface.setOverscan(Display.DEFAULT_DISPLAY, rect.left, rect.top, rect.right,
+ rect.bottom);
+ return 0;
+ }
+
+ private int runDisplayScaling(PrintWriter pw) throws RemoteException {
+ String scalingStr = getNextArgRequired();
+ if ("auto".equals(scalingStr)) {
+ mInterface.setForcedDisplayScalingMode(Display.DEFAULT_DISPLAY, 0);
+ } else if ("off".equals(scalingStr)) {
+ mInterface.setForcedDisplayScalingMode(Display.DEFAULT_DISPLAY, 1);
+ } else {
+ getErrPrintWriter().println("Error: scaling must be 'auto' or 'off'");
+ return -1;
+ }
+ return 0;
+ }
+
+ private int runSetScreenCapture(PrintWriter pw) throws RemoteException {
+ String userIdStr = getNextArg();
+ String enableStr = getNextArg();
+ int userId;
+ boolean disable;
+
+ try {
+ userId = Integer.parseInt(userIdStr);
+ } catch (NumberFormatException e) {
+ getErrPrintWriter().println("Error: bad number " + e);
+ return -1;
+ }
+
+ disable = !Boolean.parseBoolean(enableStr);
+ mInternal.setScreenCaptureDisabled(userId, disable);
+ return 0;
+ }
+
+ private int runDismissKeyguard(PrintWriter pw) throws RemoteException {
+ mInterface.dismissKeyguard(null /* callback */);
+ return 0;
+ }
+
+ private int runSurfaceTrace(PrintWriter pw) throws RemoteException {
+ final ParcelFileDescriptor pfd;
+ try {
+ pfd = ParcelFileDescriptor.dup(getOutFileDescriptor());
+ } catch (IOException e) {
+ getErrPrintWriter().println("Unable to dup output stream: " + e.getMessage());
+ return -1;
+ }
+ mInternal.enableSurfaceTrace(pfd);
+
+ // Read input until an explicit quit command is sent or the stream is closed (meaning
+ // the user killed the command).
+ try {
+ InputStream input = getRawInputStream();
+ InputStreamReader converter = new InputStreamReader(input);
+ BufferedReader in = new BufferedReader(converter);
+ String line;
+
+ while ((line = in.readLine()) != null) {
+ if (line.length() <= 0) {
+ // no-op
+ } else if ("q".equals(line) || "quit".equals(line)) {
+ break;
+ } else {
+ pw.println("Invalid command: " + line);
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace(pw);
+ } finally {
+ mInternal.disableSurfaceTrace();
+ try {
+ pfd.close();
+ } catch (IOException e) {
+ }
+ }
+
+ return 0;
+ }
+
+ private int parseDimension(String s) throws NumberFormatException {
+ if (s.endsWith("px")) {
+ return Integer.parseInt(s.substring(0, s.length() - 2));
+ }
+ if (s.endsWith("dp")) {
+ int density;
+ try {
+ density = mInterface.getBaseDisplayDensity(Display.DEFAULT_DISPLAY);
+ } catch (RemoteException e) {
+ density = DisplayMetrics.DENSITY_DEFAULT;
+ }
+ return Integer.parseInt(s.substring(0, s.length() - 2)) * density /
+ DisplayMetrics.DENSITY_DEFAULT;
}
+ return Integer.parseInt(s);
}
@Override
public void onHelp() {
PrintWriter pw = getOutPrintWriter();
- pw.println("Window Manager (window) commands:");
+ pw.println("Window manager (window) commands:");
pw.println(" help");
- pw.println(" Print this help text.");
- pw.println();
- if (!IS_USER){
+ pw.println(" Print this help text.");
+ pw.println(" size [reset|WxH|WdpxHdp]");
+ pw.println(" Return or override display size.");
+ pw.println(" width and height in pixels unless suffixed with 'dp'.");
+ pw.println(" density [reset|DENSITY]");
+ pw.println(" Return or override display density.");
+ pw.println(" overscan [reset|LEFT,TOP,RIGHT,BOTTOM]");
+ pw.println(" Set overscan area for display.");
+ pw.println(" scaling [off|auto]");
+ pw.println(" Set display scaling mode.");
+ pw.println(" screen-capture [userId] [true|false]");
+ pw.println(" Enable or disable screen capture.");
+ pw.println(" dismiss-keyguard");
+ pw.println(" Dismiss the keyguard, prompting user for auth if necessary.");
+ pw.println(" surface-trace");
+ pw.println(" Log surface commands to stdout in a binary format.");
+ if (!IS_USER) {
pw.println(" tracing (start | stop)");
- pw.println(" start or stop window tracing");
- pw.println();
+ pw.println(" Start or stop window tracing.");
}
}
}
diff --git a/com/android/server/wm/WindowManagerThreadPriorityBooster.java b/com/android/server/wm/WindowManagerThreadPriorityBooster.java
index 1b2eb465..dd89b3b2 100644
--- a/com/android/server/wm/WindowManagerThreadPriorityBooster.java
+++ b/com/android/server/wm/WindowManagerThreadPriorityBooster.java
@@ -36,6 +36,7 @@ class WindowManagerThreadPriorityBooster extends ThreadPriorityBooster {
private final Object mLock = new Object();
private final int mAnimationThreadId;
+ private final int mSurfaceAnimationThreadId;
@GuardedBy("mLock")
private boolean mAppTransitionRunning;
@@ -45,14 +46,16 @@ class WindowManagerThreadPriorityBooster extends ThreadPriorityBooster {
WindowManagerThreadPriorityBooster() {
super(THREAD_PRIORITY_DISPLAY, INDEX_WINDOW);
mAnimationThreadId = AnimationThread.get().getThreadId();
+ mSurfaceAnimationThreadId = SurfaceAnimationThread.get().getThreadId();
}
@Override
public void boost() {
- // Do not boost the animation thread. As the animation thread is changing priorities,
+ // Do not boost the animation threads. As the animation threads are changing priorities,
// boosting it might mess up the priority because we reset it the the previous priority.
- if (myTid() == mAnimationThreadId) {
+ final int myTid = myTid();
+ if (myTid == mAnimationThreadId || myTid == mSurfaceAnimationThreadId) {
return;
}
super.boost();
@@ -62,7 +65,8 @@ class WindowManagerThreadPriorityBooster extends ThreadPriorityBooster {
public void reset() {
// See comment in boost().
- if (myTid() == mAnimationThreadId) {
+ final int myTid = myTid();
+ if (myTid == mAnimationThreadId || myTid == mSurfaceAnimationThreadId) {
return;
}
super.reset();
@@ -92,5 +96,6 @@ class WindowManagerThreadPriorityBooster extends ThreadPriorityBooster {
? TOP_APP_PRIORITY_BOOST : THREAD_PRIORITY_DISPLAY;
setBoostToPriority(priority);
setThreadPriority(mAnimationThreadId, priority);
+ setThreadPriority(mSurfaceAnimationThreadId, priority);
}
}
diff --git a/com/android/server/wm/WindowState.java b/com/android/server/wm/WindowState.java
index c2ac9052..dfb385bd 100644
--- a/com/android/server/wm/WindowState.java
+++ b/com/android/server/wm/WindowState.java
@@ -19,11 +19,11 @@ package com.android.server.wm;
import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.SurfaceControl.Transaction;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE;
-import static android.view.SurfaceControl.Transaction;
import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
import static android.view.WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW;
import static android.view.WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
@@ -42,7 +42,6 @@ import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
import static android.view.WindowManager.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH;
@@ -50,6 +49,7 @@ import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
@@ -91,6 +91,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIG
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.H.SEND_NEW_CONFIGURATION;
+import static com.android.server.wm.WindowManagerService.MAX_ANIMATION_DURATION;
import static com.android.server.wm.WindowManagerService.TYPE_LAYER_MULTIPLIER;
import static com.android.server.wm.WindowManagerService.TYPE_LAYER_OFFSET;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
@@ -117,7 +118,9 @@ import static com.android.server.wm.proto.WindowStateProto.GIVEN_CONTENT_INSETS;
import static com.android.server.wm.proto.WindowStateProto.IDENTIFIER;
import static com.android.server.wm.proto.WindowStateProto.PARENT_FRAME;
import static com.android.server.wm.proto.WindowStateProto.STACK_ID;
+import static com.android.server.wm.proto.WindowStateProto.SHOWN_POSITION;
import static com.android.server.wm.proto.WindowStateProto.SURFACE_INSETS;
+import static com.android.server.wm.proto.WindowStateProto.SURFACE_POSITION;
import static com.android.server.wm.proto.WindowStateProto.WINDOW_CONTAINER;
import android.annotation.CallSuper;
@@ -144,6 +147,7 @@ import android.util.MergedConfiguration;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
+import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.IApplicationToken;
@@ -159,10 +163,14 @@ import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowInfo;
import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
import com.android.internal.util.ToBooleanFunction;
import com.android.server.input.InputWindowHandle;
import com.android.server.policy.WindowManagerPolicy;
+import com.android.server.wm.LocalAnimationAdapter.AnimationSpec;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
@@ -185,7 +193,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// to capture touch events in that area.
static final int RESIZE_HANDLE_WIDTH_IN_DP = 30;
- final WindowManagerService mService;
final WindowManagerPolicy mPolicy;
final Context mContext;
final Session mSession;
@@ -320,6 +327,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
private final Rect mLastOutsets = new Rect();
private boolean mOutsetsChanged = false;
+ /** Part of the display that has been cut away. See {@link DisplayCutout}. */
+ DisplayCutout mDisplayCutout = DisplayCutout.NO_CUTOUT;
+ private DisplayCutout mLastDisplayCutout = DisplayCutout.NO_CUTOUT;
+ private boolean mDisplayCutoutChanged;
+
/**
* Set to true if we are waiting for this window to receive its
* given internal insets before laying out other windows based on it.
@@ -589,6 +601,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
*/
private boolean mDrawnStateEvaluated;
+ private final Point mSurfacePosition = new Point();
+
/**
* Compares two window sub-layers and returns -1 if the first is lesser than the second in terms
* of z-order and 1 otherwise.
@@ -621,7 +635,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token,
WindowState parentWindow, int appOp, int seq, WindowManager.LayoutParams a,
int viewVisibility, int ownerId, boolean ownerCanAddInternalSystemWindow) {
- mService = service;
+ super(service);
mSession = s;
mClient = c;
mAppOp = appOp;
@@ -772,7 +786,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
@Override
public void computeFrameLw(Rect parentFrame, Rect displayFrame, Rect overscanFrame,
Rect contentFrame, Rect visibleFrame, Rect decorFrame, Rect stableFrame,
- Rect outsetFrame) {
+ Rect outsetFrame, DisplayCutout displayCutout) {
if (mWillReplaceWindow && (mAnimatingExit || !mReplacingRemoveRequested)) {
// This window is being replaced and either already got information that it's being
// removed or we are still waiting for some information. Because of this we don't
@@ -1009,6 +1023,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mVisibleFrame.offset(-layoutXDiff, -layoutYDiff);
mStableFrame.offset(-layoutXDiff, -layoutYDiff);
+ // TODO(roosa): Figure out what frame exactly this needs to be calculated with.
+ mDisplayCutout = displayCutout.calculateRelativeTo(mFrame);
+
+
mCompatFrame.set(mFrame);
if (mEnforceSizeCompat) {
// If there is a size compatibility scale being applied to the
@@ -1149,8 +1167,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mOutsetsChanged |= !mLastOutsets.equals(mOutsets);
mFrameSizeChanged |= (mLastFrame.width() != mFrame.width()) ||
(mLastFrame.height() != mFrame.height());
+ mDisplayCutoutChanged |= !mLastDisplayCutout.equals(mDisplayCutout);
return mOverscanInsetsChanged || mContentInsetsChanged || mVisibleInsetsChanged
- || mOutsetsChanged || mFrameSizeChanged;
+ || mOutsetsChanged || mFrameSizeChanged || mDisplayCutoutChanged;
}
/**
@@ -1195,6 +1214,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
|| winAnimator.mSurfaceResized
|| mOutsetsChanged
|| mFrameSizeChanged
+ || mDisplayCutoutChanged
|| configChanged
|| dragResizingChanged
|| !isResizedWhileNotDragResizingReported()
@@ -1214,7 +1234,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
+ " dragResizingChanged=" + dragResizingChanged
+ " resizedWhileNotDragResizingReported="
+ isResizedWhileNotDragResizingReported()
- + " reportOrientationChanged=" + mReportOrientationChanged);
+ + " reportOrientationChanged=" + mReportOrientationChanged
+ + " displayCutoutChanged=" + mDisplayCutoutChanged);
}
// If it's a dead window left on screen, and the configuration changed, there is nothing
@@ -1411,7 +1432,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
*/
// TODO: Can we consolidate this with #isVisible() or have a more appropriate name for this?
boolean isWinVisibleLw() {
- return (mAppToken == null || !mAppToken.hiddenRequested || mAppToken.mAppAnimator.animating)
+ return (mAppToken == null || !mAppToken.hiddenRequested || mAppToken.isSelfAnimating())
&& isVisible();
}
@@ -1420,7 +1441,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
* not the pending requested hidden state.
*/
boolean isVisibleNow() {
- return (!mToken.hidden || mAttrs.type == TYPE_APPLICATION_STARTING)
+ return (!mToken.isHidden() || mAttrs.type == TYPE_APPLICATION_STARTING)
&& isVisible();
}
@@ -1458,9 +1479,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
final AppWindowToken atoken = mAppToken;
if (atoken != null) {
return ((!isParentWindowHidden() && !atoken.hiddenRequested)
- || mWinAnimator.mAnimation != null || atoken.mAppAnimator.animation != null);
+ || mWinAnimator.isAnimationSet());
}
- return !isParentWindowHidden() || mWinAnimator.mAnimation != null;
+ return !isParentWindowHidden() || mWinAnimator.isAnimationSet();
}
/**
@@ -1480,7 +1501,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
*/
boolean isInteresting() {
return mAppToken != null && !mAppDied
- && (!mAppToken.mAppAnimator.freezingScreen || !mAppFreezing);
+ && (!mAppToken.isFreezingScreen() || !mAppFreezing);
}
/**
@@ -1492,34 +1513,21 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
return false;
}
return mHasSurface && mPolicyVisibility && !mDestroying
- && ((!isParentWindowHidden() && mViewVisibility == View.VISIBLE && !mToken.hidden)
- || mWinAnimator.mAnimation != null
- || ((mAppToken != null) && (mAppToken.mAppAnimator.animation != null)));
+ && ((!isParentWindowHidden() && mViewVisibility == View.VISIBLE && !mToken.isHidden())
+ || mWinAnimator.isAnimationSet());
}
// TODO: Another visibility method that was added late in the release to minimize risk.
@Override
public boolean canAffectSystemUiFlags() {
- final boolean shown = mWinAnimator.getShown();
-
- // We only consider the app to be exiting when the animation has started. After the app
- // transition is executed the windows are marked exiting before the new windows have been
- // shown. Thus, wait considering a window to be exiting after the animation has actually
- // started.
- final boolean appAnimationStarting = mAppToken != null
- && mAppToken.mAppAnimator.isAnimationStarting();
- final boolean exitingSelf = mAnimatingExit && (!mWinAnimator.isAnimationStarting()
- && !appAnimationStarting);
- final boolean appExiting = mAppToken != null && mAppToken.hidden && !appAnimationStarting;
-
- final boolean exiting = exitingSelf || mDestroying || appExiting;
final boolean translucent = mAttrs.alpha == 0.0f;
-
- // If we are entering with a dummy animation, avoid affecting SystemUI flags until the
- // transition is starting.
- final boolean enteringWithDummyAnimation =
- mWinAnimator.isDummyAnimation() && mWinAnimator.mShownAlpha == 0f;
- return shown && !exiting && !translucent && !enteringWithDummyAnimation;
+ if (mAppToken == null) {
+ final boolean shown = mWinAnimator.getShown();
+ final boolean exiting = mAnimatingExit || mDestroying;
+ return shown && !exiting && !translucent;
+ } else {
+ return !mAppToken.isHidden();
+ }
}
/**
@@ -1530,10 +1538,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
public boolean isDisplayedLw() {
final AppWindowToken atoken = mAppToken;
return isDrawnLw() && mPolicyVisibility
- && ((!isParentWindowHidden() &&
- (atoken == null || !atoken.hiddenRequested))
- || mWinAnimator.mAnimating
- || (atoken != null && atoken.mAppAnimator.animation != null));
+ && ((!isParentWindowHidden() && (atoken == null || !atoken.hiddenRequested))
+ || mWinAnimator.isAnimationSet());
}
/**
@@ -1541,8 +1547,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
*/
@Override
public boolean isAnimatingLw() {
- return mWinAnimator.mAnimation != null
- || (mAppToken != null && mAppToken.mAppAnimator.animation != null);
+ return isAnimating();
}
@Override
@@ -1550,7 +1555,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
final AppWindowToken atoken = mAppToken;
return mViewVisibility == View.GONE
|| !mRelayoutCalled
- || (atoken == null && mToken.hidden)
+ || (atoken == null && mToken.isHidden())
|| (atoken != null && atoken.hiddenRequested)
|| isParentWindowHidden()
|| (mAnimatingExit && !isAnimatingLw())
@@ -1588,8 +1593,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// to determine if it's occluding apps.
return ((!mIsWallpaper && mAttrs.format == PixelFormat.OPAQUE)
|| (mIsWallpaper && mWallpaperVisible))
- && isDrawnLw() && mWinAnimator.mAnimation == null
- && (mAppToken == null || mAppToken.mAppAnimator.animation == null);
+ && isDrawnLw() && !mWinAnimator.isAnimationSet();
}
@Override
@@ -1611,7 +1615,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// Starting window that's exiting will be removed when the animation finishes.
// Mark all relevant flags for that onExitAnimationDone will proceed all the way
// to actually remove it.
- if (!visible && isVisibleNow() && mAppToken.mAppAnimator.isAnimating()) {
+ if (!visible && isVisibleNow() && mAppToken.isSelfAnimating()) {
mAnimatingExit = true;
mRemoveOnExit = true;
mWindowRemovalAllowed = true;
@@ -1720,7 +1724,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
* listeners and optionally animate it. Simply checking a change of position is not enough,
* because being move due to dock divider is not a trigger for animation.
*/
- void handleWindowMovedIfNeeded() {
+ void handleWindowMovedIfNeeded(Transaction t) {
if (!hasMoved()) {
return;
}
@@ -1738,7 +1742,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
&& !isDragResizing() && !adjustedForMinimizedDockOrIme
&& getWindowConfiguration().hasMovementAnimations()
&& !mWinAnimator.mLastHidden) {
- mWinAnimator.setMoveAnimation(left, top);
+ startMoveAnimation(t, left, top);
}
//TODO (multidisplay): Accessibility supported only for the default display.
@@ -1888,7 +1892,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
+ " surfaceShowing=" + mWinAnimator.getShown()
+ " isAnimationSet=" + mWinAnimator.isAnimationSet()
+ " app-animation="
- + (mAppToken != null ? mAppToken.mAppAnimator.animation : null)
+ + (mAppToken != null ? mAppToken.isSelfAnimating() : "false")
+ " mWillReplaceWindow=" + mWillReplaceWindow
+ " inPendingTransaction="
+ (mAppToken != null ? mAppToken.inPendingTransaction : false)
@@ -1948,8 +1952,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mService.mAccessibilityController.onWindowTransitionLocked(this, transit);
}
}
- final boolean isAnimating =
- mWinAnimator.isAnimationSet() && !mWinAnimator.isDummyAnimation();
+ final boolean isAnimating = mWinAnimator.isAnimationSet()
+ && (mAppToken == null || !mAppToken.isWaitingForTransitionStart());
final boolean lastWindowIsStartingWindow = startingWindow && mAppToken != null
&& mAppToken.isLastWindow(this);
// We delay the removal of a window if it has a showing surface that can be used to run
@@ -1999,28 +2003,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mHasSurface = hasSurface;
}
- int getAnimLayerAdjustment() {
- if (mIsImWindow && mService.mInputMethodTarget != null) {
- final AppWindowToken appToken = mService.mInputMethodTarget.mAppToken;
- if (appToken != null) {
- return appToken.getAnimLayerAdjustment();
- }
- }
-
- return mToken.getAnimLayerAdjustment();
- }
-
- int getSpecialWindowAnimLayerAdjustment() {
- int specialAdjustment = 0;
- if (mIsImWindow) {
- specialAdjustment = getDisplayContent().mInputMethodAnimLayerAdjustment;
- } else if (mIsWallpaper) {
- specialAdjustment = getDisplayContent().mWallpaperController.getAnimLayerAdjustment();
- }
-
- return mLayer + specialAdjustment;
- }
-
boolean canBeImeTarget() {
if (mIsImWindow) {
// IME windows can't be IME targets. IME targets are required to be below the IME
@@ -2445,10 +2427,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
if (DEBUG_VISIBILITY) Slog.v(TAG, "Policy visibility true: " + this);
if (doAnimation) {
if (DEBUG_VISIBILITY) Slog.v(TAG, "doAnimation: mPolicyVisibility="
- + mPolicyVisibility + " mAnimation=" + mWinAnimator.mAnimation);
+ + mPolicyVisibility + " isAnimationSet=" + mWinAnimator.isAnimationSet());
if (!mToken.okToAnimate()) {
doAnimation = false;
- } else if (mPolicyVisibility && mWinAnimator.mAnimation == null) {
+ } else if (mPolicyVisibility && !mWinAnimator.isAnimationSet()) {
// Check for the case where we are currently visible and
// not animating; we do not want to do animation at such a
// point to become visible when we already are.
@@ -2487,7 +2469,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
}
if (doAnimation) {
mWinAnimator.applyAnimationLocked(TRANSIT_EXIT, false);
- if (mWinAnimator.mAnimation == null) {
+ if (!mWinAnimator.isAnimationSet()) {
doAnimation = false;
}
}
@@ -2587,14 +2569,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
return mAnimatingExit || (mService.mClosingApps.contains(mAppToken));
}
- @Override
- boolean isAnimating() {
- if (mWinAnimator.isAnimationSet() || mAnimatingExit) {
- return true;
- }
- return super.isAnimating();
- }
-
void addWinAnimatorToList(ArrayList<WindowStateAnimator> animators) {
animators.add(mWinAnimator);
@@ -2849,6 +2823,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
final boolean reportDraw = mWinAnimator.mDrawState == DRAW_PENDING;
final boolean reportOrientation = mReportOrientationChanged;
final int displayId = getDisplayId();
+ final DisplayCutout displayCutout = mDisplayCutout;
if (mAttrs.type != WindowManager.LayoutParams.TYPE_APPLICATION_STARTING
&& mClient instanceof IWindow.Stub) {
// To prevent deadlock simulate one-way call if win.mClient is a local object.
@@ -2858,7 +2833,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
try {
dispatchResized(frame, overscanInsets, contentInsets, visibleInsets,
stableInsets, outsets, reportDraw, mergedConfiguration,
- reportOrientation, displayId);
+ reportOrientation, displayId, displayCutout);
} catch (RemoteException e) {
// Not a remote call, RemoteException won't be raised.
}
@@ -2866,7 +2841,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
});
} else {
dispatchResized(frame, overscanInsets, contentInsets, visibleInsets, stableInsets,
- outsets, reportDraw, mergedConfiguration, reportOrientation, displayId);
+ outsets, reportDraw, mergedConfiguration, reportOrientation, displayId,
+ displayCutout);
}
//TODO (multidisplay): Accessibility supported only for the default display.
@@ -2880,6 +2856,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mStableInsetsChanged = false;
mOutsetsChanged = false;
mFrameSizeChanged = false;
+ mDisplayCutoutChanged = false;
mResizedWhileNotDragResizingReported = true;
mWinAnimator.mSurfaceResized = false;
mReportOrientationChanged = false;
@@ -2922,14 +2899,16 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
private void dispatchResized(Rect frame, Rect overscanInsets, Rect contentInsets,
Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw,
- MergedConfiguration mergedConfiguration, boolean reportOrientation, int displayId)
+ MergedConfiguration mergedConfiguration, boolean reportOrientation, int displayId,
+ DisplayCutout displayCutout)
throws RemoteException {
final boolean forceRelayout = isDragResizeChanged() || mResizedWhileNotDragResizing
|| reportOrientation;
mClient.resized(frame, overscanInsets, contentInsets, visibleInsets, stableInsets, outsets,
reportDraw, mergedConfiguration, getBackdropFrame(frame), forceRelayout,
- mPolicy.isNavBarForcedShownLw(this), displayId);
+ mPolicy.isNavBarForcedShownLw(this), displayId,
+ new DisplayCutout.ParcelableWrapper(displayCutout));
mDragResizingChangeReported = true;
}
@@ -3097,6 +3076,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mContentFrame.writeToProto(proto, CONTENT_FRAME);
mContentInsets.writeToProto(proto, CONTENT_INSETS);
mAttrs.surfaceInsets.writeToProto(proto, SURFACE_INSETS);
+ mSurfacePosition.writeToProto(proto, SURFACE_POSITION);
+ mShownPosition.writeToProto(proto, SHOWN_POSITION);
mWinAnimator.writeToProto(proto, ANIMATOR);
proto.write(ANIMATING_EXIT, mAnimatingExit);
for (int i = 0; i < mChildren.size(); i++) {
@@ -3116,6 +3097,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
proto.end(token);
}
+ @Override
void dump(PrintWriter pw, String prefix, boolean dumpAll) {
final TaskStack stack = getStack();
pw.print(prefix); pw.print("mDisplayId="); pw.print(getDisplayId());
@@ -3150,7 +3132,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
pw.print(prefix); pw.print("mBaseLayer="); pw.print(mBaseLayer);
pw.print(" mSubLayer="); pw.print(mSubLayer);
pw.print(" mAnimLayer="); pw.print(mLayer); pw.print("+");
- pw.print(getAnimLayerAdjustment());
pw.print("="); pw.print(mWinAnimator.mAnimLayer);
pw.print(" mLastLayer="); pw.println(mWinAnimator.mLastLayer);
}
@@ -3246,6 +3227,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
pw.print(" stable="); mStableInsets.printShortString(pw);
pw.print(" surface="); mAttrs.surfaceInsets.printShortString(pw);
pw.print(" outsets="); mOutsets.printShortString(pw);
+ pw.print(" cutout=" + mDisplayCutout);
pw.println();
pw.print(prefix); pw.print("Lst insets: overscan=");
mLastOverscanInsets.printShortString(pw);
@@ -3254,8 +3236,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
pw.print(" stable="); mLastStableInsets.printShortString(pw);
pw.print(" physical="); mLastOutsets.printShortString(pw);
pw.print(" outset="); mLastOutsets.printShortString(pw);
+ pw.print(" cutout=" + mLastDisplayCutout);
pw.println();
}
+ super.dump(pw, prefix, dumpAll);
pw.print(prefix); pw.print(mWinAnimator); pw.println(":");
mWinAnimator.dump(pw, prefix + " ", dumpAll);
if (mAnimatingExit || mRemoveOnExit || mDestroying || mRemoved) {
@@ -3314,7 +3298,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
@Override
String getName() {
return Integer.toHexString(System.identityHashCode(this))
- + " " + getWindowTag();
+ + " " + getWindowTag();
}
CharSequence getWindowTag() {
@@ -3674,10 +3658,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
+ " parentHidden=" + isParentWindowHidden()
+ " tok.hiddenRequested="
+ (mAppToken != null && mAppToken.hiddenRequested)
- + " tok.hidden=" + (mAppToken != null && mAppToken.hidden)
- + " animating=" + mWinAnimator.mAnimating
+ + " tok.hidden=" + (mAppToken != null && mAppToken.isHidden())
+ + " animationSet=" + mWinAnimator.isAnimationSet()
+ " tok animating="
- + (mWinAnimator.mAppAnimator != null && mWinAnimator.mAppAnimator.animating)
+ + (mAppToken != null && mAppToken.isSelfAnimating())
+ " Callers=" + Debug.getCallers(4));
}
}
@@ -3881,23 +3865,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
return null;
}
- boolean isWindowAnimationSet() {
- if (mWinAnimator.isWindowAnimationSet()) {
- return true;
- }
- for (int i = mChildren.size() - 1; i >= 0; --i) {
- final WindowState c = mChildren.get(i);
- if (c.isWindowAnimationSet()) {
- return true;
- }
- }
- return false;
- }
-
void onExitAnimationDone() {
if (DEBUG_ANIM) Slog.v(TAG, "onExitAnimationDone in " + this
+ ": exiting=" + mAnimatingExit + " remove=" + mRemoveOnExit
- + " windowAnimating=" + mWinAnimator.isWindowAnimationSet());
+ + " selfAnimating=" + isSelfAnimating());
if (!mChildren.isEmpty()) {
// Copying to a different list as multiple children can be removed.
@@ -3921,18 +3892,16 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
}
}
- if (!mWinAnimator.isWindowAnimationSet()) {
- //TODO (multidisplay): Accessibility is supported only for the default display.
- if (mService.mAccessibilityController != null && getDisplayId() == DEFAULT_DISPLAY) {
- mService.mAccessibilityController.onSomeWindowResizedOrMovedLocked();
- }
+ if (isSelfAnimating()) {
+ return;
}
- if (!mAnimatingExit) {
- return;
+ //TODO (multidisplay): Accessibility is supported only for the default display.
+ if (mService.mAccessibilityController != null && getDisplayId() == DEFAULT_DISPLAY) {
+ mService.mAccessibilityController.onSomeWindowResizedOrMovedLocked();
}
- if (mWinAnimator.isWindowAnimationSet()) {
+ if (!mAnimatingExit) {
return;
}
@@ -3986,10 +3955,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mAnimatingExit = false;
didSomething = true;
}
- if (mWinAnimator.mAnimating) {
- mWinAnimator.mAnimating = false;
- didSomething = true;
- }
if (mDestroying) {
mDestroying = false;
mService.mDestroySurface.remove(this);
@@ -4078,7 +4043,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
+ " mDrawState=" + mWinAnimator.mDrawState
+ " ph=" + isParentWindowHidden()
+ " th=" + (mAppToken != null ? mAppToken.hiddenRequested : false)
- + " a=" + mWinAnimator.mAnimating);
+ + " a=" + mWinAnimator.isAnimationSet());
}
}
@@ -4278,6 +4243,60 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mLastVisibleInsets.set(mVisibleInsets);
mLastStableInsets.set(mStableInsets);
mLastOutsets.set(mOutsets);
+ mLastDisplayCutout = mDisplayCutout;
+ }
+
+ void startAnimation(Animation anim) {
+ final DisplayInfo displayInfo = getDisplayContent().getDisplayInfo();
+ anim.initialize(mFrame.width(), mFrame.height(),
+ displayInfo.appWidth, displayInfo.appHeight);
+ anim.restrictDuration(MAX_ANIMATION_DURATION);
+ anim.scaleCurrentDuration(mService.getWindowAnimationScaleLocked());
+ final AnimationAdapter adapter = new LocalAnimationAdapter(
+ new WindowAnimationSpec(anim, mSurfacePosition, false /* canSkipFirstFrame */),
+ mService.mSurfaceAnimationRunner);
+ startAnimation(mPendingTransaction, adapter);
+ commitPendingTransaction();
+ }
+
+ private void startMoveAnimation(Transaction t, int left, int top) {
+ if (DEBUG_ANIM) Slog.v(TAG, "Setting move animation on " + this);
+ final Point oldPosition = new Point();
+ final Point newPosition = new Point();
+ transformFrameToSurfacePosition(mLastFrame.left, mLastFrame.top, oldPosition);
+ transformFrameToSurfacePosition(left, top, newPosition);
+ final AnimationAdapter adapter = new LocalAnimationAdapter(
+ new MoveAnimationSpec(oldPosition.x, oldPosition.y, newPosition.x, newPosition.y),
+ mService.mSurfaceAnimationRunner);
+ startAnimation(t, adapter);
+ }
+
+ private void startAnimation(Transaction t, AnimationAdapter adapter) {
+ startAnimation(t, adapter, mWinAnimator.mLastHidden);
+ }
+
+ @Override
+ protected void onAnimationFinished() {
+ mWinAnimator.onAnimationFinished();
+ }
+
+ /**
+ * Retrieves the current transformation matrix of the window, relative to the display.
+ *
+ * @param float9 A temporary array of 9 floats.
+ * @param outMatrix Matrix to fill in the transformation.
+ */
+ void getTransformationMatrix(float[] float9, Matrix outMatrix) {
+ float9[Matrix.MSCALE_X] = mWinAnimator.mDsDx;
+ float9[Matrix.MSKEW_Y] = mWinAnimator.mDtDx;
+ float9[Matrix.MSKEW_X] = mWinAnimator.mDtDy;
+ float9[Matrix.MSCALE_Y] = mWinAnimator.mDsDy;
+ float9[Matrix.MTRANS_X] = mSurfacePosition.x + mShownPosition.x;
+ float9[Matrix.MTRANS_Y] = mSurfacePosition.y + mShownPosition.y;
+ float9[Matrix.MPERSP_0] = 0;
+ float9[Matrix.MPERSP_1] = 0;
+ float9[Matrix.MPERSP_2] = 1;
+ outMatrix.setValues(float9);
}
// TODO: Hack to work around the number of states AppWindowToken needs to access without having
@@ -4331,15 +4350,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
}
}
- boolean usesRelativeZOrdering() {
- if (!isChildWindow()) {
- return false;
- } else if (mAttrs.type == TYPE_APPLICATION_MEDIA_OVERLAY) {
- return true;
- } else {
- return false;
- }
- }
@Override
boolean shouldMagnify() {
@@ -4367,12 +4377,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
@Override
boolean needsZBoost() {
- return getAnimLayerAdjustment() > 0 || mWillReplaceWindow;
- }
-
- @Override
- SurfaceControl.Builder makeSurface() {
- return getParent().makeChildSurface(this);
+ if (mIsImWindow && mService.mInputMethodTarget != null) {
+ final AppWindowToken appToken = mService.mInputMethodTarget.mAppToken;
+ if (appToken != null) {
+ return appToken.needsZBoost();
+ }
+ }
+ return mWillReplaceWindow;
}
private void applyDims(Dimmer dimmer) {
@@ -4394,11 +4405,60 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
applyDims(dimmer);
}
+ updateSurfacePosition(mPendingTransaction);
+
mWinAnimator.prepareSurfaceLocked(true);
super.prepareSurfaces();
}
@Override
+ public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
+ super.onAnimationLeashCreated(t, leash);
+
+ // Leash is now responsible for position, so set our position to 0.
+ t.setPosition(mSurfaceControl, 0, 0);
+ }
+
+ @Override
+ public void onAnimationLeashDestroyed(Transaction t) {
+ super.onAnimationLeashDestroyed(t);
+ updateSurfacePosition(t);
+ }
+
+ @Override
+ void updateSurfacePosition(Transaction t) {
+ if (mSurfaceControl == null) {
+ return;
+ }
+
+ transformFrameToSurfacePosition(mFrame.left, mFrame.top, mSurfacePosition);
+ if (!mSurfaceAnimator.hasLeash()) {
+ t.setPosition(mSurfaceControl, mSurfacePosition.x, mSurfacePosition.y);
+ }
+ }
+
+ private void transformFrameToSurfacePosition(int left, int top, Point outPoint) {
+ outPoint.set(left, top);
+ final WindowContainer parentWindowContainer = getParent();
+ if (isChildWindow()) {
+ // TODO: This probably falls apart at some point and we should
+ // actually compute relative coordinates.
+
+ // Since the parent was outset by its surface insets, we need to undo the outsetting
+ // with insetting by the same amount.
+ final WindowState parent = getParentWindow();
+ outPoint.offset(-parent.mFrame.left + parent.mAttrs.surfaceInsets.left,
+ -parent.mFrame.top + parent.mAttrs.surfaceInsets.top);
+ } else if (parentWindowContainer != null) {
+ final Rect parentBounds = parentWindowContainer.getBounds();
+ outPoint.offset(-parentBounds.left, -parentBounds.top);
+ }
+
+ // Expand for surface insets. See WindowState.expandForSurfaceInsets.
+ outPoint.offset(-mAttrs.surfaceInsets.left, -mAttrs.surfaceInsets.top);
+ }
+
+ @Override
void assignLayer(Transaction t, int layer) {
// See comment in assignRelativeLayerForImeTargetChild
if (!isChildWindow()
@@ -4414,4 +4474,58 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
public boolean isDimming() {
return mIsDimming;
}
+
+ // TODO(b/70040778): We should aim to eliminate the last user of TYPE_APPLICATION_MEDIA
+ // then we can drop all negative layering on the windowing side and simply inherit
+ // the default implementation here.
+ public void assignChildLayers(Transaction t) {
+ int layer = 1;
+ for (int i = 0; i < mChildren.size(); i++) {
+ final WindowState w = mChildren.get(i);
+
+ // APPLICATION_MEDIA_OVERLAY needs to go above APPLICATION_MEDIA
+ // while they both need to go below the main window. However the
+ // relative layering of multiple APPLICATION_MEDIA/OVERLAY has never
+ // been defined and so we can use static layers and leave it that way.
+ if (w.mAttrs.type == TYPE_APPLICATION_MEDIA) {
+ w.assignLayer(t, -2);
+ } else if (w.mAttrs.type == TYPE_APPLICATION_MEDIA_OVERLAY) {
+ w.assignLayer(t, -1);
+ } else {
+ w.assignLayer(t, layer);
+ }
+ w.assignChildLayers(t);
+ layer++;
+ }
+ }
+
+ private final class MoveAnimationSpec implements AnimationSpec {
+
+ private final long mDuration;
+ private Interpolator mInterpolator;
+ private Point mFrom = new Point();
+ private Point mTo = new Point();
+
+ private MoveAnimationSpec(int fromX, int fromY, int toX, int toY) {
+ final Animation anim = AnimationUtils.loadAnimation(mContext,
+ com.android.internal.R.anim.window_move_from_decor);
+ mDuration = anim.computeDurationHint();
+ mInterpolator = anim.getInterpolator();
+ mFrom.set(fromX, fromY);
+ mTo.set(toX, toY);
+ }
+
+ @Override
+ public long getDuration() {
+ return mDuration;
+ }
+
+ @Override
+ public void apply(Transaction t, SurfaceControl leash, long currentPlayTime) {
+ final float fraction = (float) currentPlayTime / getDuration();
+ final float v = mInterpolator.getInterpolation(fraction);
+ t.setPosition(leash, mFrom.x + (mTo.x - mFrom.x) * v,
+ mFrom.y + (mTo.y - mFrom.y) * v);
+ }
+ }
}
diff --git a/com/android/server/wm/WindowStateAnimator.java b/com/android/server/wm/WindowStateAnimator.java
index 4b5661f5..d2247ac2 100644
--- a/com/android/server/wm/WindowStateAnimator.java
+++ b/com/android/server/wm/WindowStateAnimator.java
@@ -23,16 +23,15 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CO
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
-import static com.android.server.wm.AppWindowAnimator.sDummyAnimation;
+import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
+import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_FREEFORM;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYERS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ORIENTATION;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WINDOW;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WINDOW_VERBOSE;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_VISIBILITY;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_CROP;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_SURFACE_ALLOC;
@@ -40,14 +39,12 @@ import static com.android.server.wm.WindowManagerDebugConfig.SHOW_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.TYPE_LAYER_MULTIPLIER;
-import static com.android.server.wm.WindowManagerService.localLOGV;
import static com.android.server.wm.WindowManagerService.logWithStack;
import static com.android.server.wm.WindowSurfacePlacer.SET_ORIENTATION_CHANGE_COMPLETE;
import static com.android.server.wm.WindowSurfacePlacer.SET_TURN_ON_SCREEN;
import static com.android.server.wm.proto.WindowStateAnimatorProto.LAST_CLIP_RECT;
import static com.android.server.wm.proto.WindowStateAnimatorProto.SURFACE;
-import android.app.WindowConfiguration;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
@@ -65,14 +62,12 @@ import android.view.SurfaceControl;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.animation.Animation;
-import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
-import android.view.animation.Transformation;
import com.android.server.policy.WindowManagerPolicy;
-import java.io.PrintWriter;
import java.io.FileDescriptor;
+import java.io.PrintWriter;
/**
* Keep track of animations and surface operations for a single WindowState.
@@ -105,27 +100,15 @@ class WindowStateAnimator {
final WindowState mWin;
private final WindowStateAnimator mParentWinAnimator;
final WindowAnimator mAnimator;
- AppWindowAnimator mAppAnimator;
final Session mSession;
final WindowManagerPolicy mPolicy;
final Context mContext;
final boolean mIsWallpaper;
private final WallpaperController mWallpaperControllerLocked;
- // Currently running animation.
- boolean mAnimating;
- boolean mLocalAnimating;
- Animation mAnimation;
boolean mAnimationIsEntrance;
- boolean mHasTransformation;
- boolean mHasLocalTransformation;
- final Transformation mTransformation = new Transformation();
- boolean mWasAnimating; // Were we animating going into the most recent animation step?
int mAnimLayer;
int mLastLayer;
- long mAnimationStartTime;
- long mLastAnimationTime;
- int mStackClip = STACK_CLIP_BEFORE_ANIM;
/**
* Set when we have changed the size of the surface, to know that
@@ -168,13 +151,6 @@ class WindowStateAnimator {
private final Rect mSystemDecorRect = new Rect();
private final Rect mLastSystemDecorRect = new Rect();
- // Used to save animation distances between the time they are calculated and when they are used.
- private int mAnimDx;
- private int mAnimDy;
-
- /** Is the next animation to be started a window move animation? */
- private boolean mAnimateMove = false;
-
float mDsDx=1, mDtDx=0, mDsDy=0, mDtDy=1;
private float mLastDsDx=1, mLastDtDx=0, mLastDsDy=0, mLastDtDy=1;
@@ -226,8 +202,6 @@ class WindowStateAnimator {
int mAttrType;
- static final long PENDING_TRANSACTION_FINISH_WAIT_TIME = 100;
-
boolean mForceScaleUntilResize;
// WindowState.mHScale and WindowState.mVScale contain the
@@ -247,93 +221,28 @@ class WindowStateAnimator {
mAnimator = service.mAnimator;
mPolicy = service.mPolicy;
mContext = service.mContext;
- final DisplayContent displayContent = win.getDisplayContent();
- if (displayContent != null) {
- final DisplayInfo displayInfo = displayContent.getDisplayInfo();
- mAnimDx = displayInfo.appWidth;
- mAnimDy = displayInfo.appHeight;
- } else {
- Slog.w(TAG, "WindowStateAnimator ctor: Display has been removed");
- // This is checked on return and dealt with.
- }
mWin = win;
mParentWinAnimator = !win.isChildWindow() ? null : win.getParentWindow().mWinAnimator;
- mAppAnimator = win.mAppToken == null ? null : win.mAppToken.mAppAnimator;
mSession = win.mSession;
mAttrType = win.mAttrs.type;
mIsWallpaper = win.mIsWallpaper;
mWallpaperControllerLocked = mService.mRoot.mWallpaperController;
}
- public void setAnimation(Animation anim, long startTime, int stackClip) {
- if (localLOGV) Slog.v(TAG, "Setting animation in " + this + ": " + anim);
- mAnimating = false;
- mLocalAnimating = false;
- mAnimation = anim;
- mAnimation.restrictDuration(WindowManagerService.MAX_ANIMATION_DURATION);
- mAnimation.scaleCurrentDuration(mService.getWindowAnimationScaleLocked());
- // Start out animation gone if window is gone, or visible if window is visible.
- mTransformation.clear();
- mTransformation.setAlpha(mLastHidden ? 0 : 1);
- mHasLocalTransformation = true;
- mAnimationStartTime = startTime;
- mStackClip = stackClip;
- }
-
- public void setAnimation(Animation anim, int stackClip) {
- setAnimation(anim, -1, stackClip);
- }
-
- public void setAnimation(Animation anim) {
- setAnimation(anim, -1, STACK_CLIP_AFTER_ANIM);
- }
-
- public void clearAnimation() {
- if (mAnimation != null) {
- mAnimating = true;
- mLocalAnimating = false;
- mAnimation.cancel();
- mAnimation = null;
- mStackClip = STACK_CLIP_BEFORE_ANIM;
- }
- }
-
/**
* Is the window or its container currently set to animate or currently animating?
*/
boolean isAnimationSet() {
- return mAnimation != null
- || (mParentWinAnimator != null && mParentWinAnimator.mAnimation != null)
- || (mAppAnimator != null && mAppAnimator.isAnimating());
- }
-
- /**
- * @return whether an animation is about to start, i.e. the animation is set already but we
- * haven't processed the first frame yet.
- */
- boolean isAnimationStarting() {
- return isAnimationSet() && !mAnimating;
- }
-
- /** Is the window animating the DummyAnimation? */
- boolean isDummyAnimation() {
- return mAppAnimator != null
- && mAppAnimator.animation == sDummyAnimation;
- }
-
- /**
- * Is this window currently set to animate or currently animating?
- */
- boolean isWindowAnimationSet() {
- return mAnimation != null;
+ return mWin.isAnimating();
}
/**
* Is this window currently waiting to run an opening animation?
*/
boolean isWaitingForOpening() {
- return mService.mAppTransition.isTransitionSet() && isDummyAnimation()
+ return mService.mAppTransition.isTransitionSet()
+ && (mWin.mAppToken != null && mWin.mAppToken.isHidden())
&& mService.mOpeningApps.contains(mWin.mAppToken);
}
@@ -341,130 +250,23 @@ class WindowStateAnimator {
if (DEBUG_ANIM) Slog.d(TAG,
"cancelExitAnimationForNextAnimationLocked: " + mWin);
- if (mAnimation != null) {
- mAnimation.cancel();
- mAnimation = null;
- mLocalAnimating = false;
- mWin.destroySurfaceUnchecked();
- }
- }
-
- private boolean stepAnimation(long currentTime) {
- if ((mAnimation == null) || !mLocalAnimating) {
- return false;
- }
- currentTime = getAnimationFrameTime(mAnimation, currentTime);
- mTransformation.clear();
- final boolean more = mAnimation.getTransformation(currentTime, mTransformation);
- if (mAnimationStartDelayed && mAnimationIsEntrance) {
- mTransformation.setAlpha(0f);
- }
- if (false && DEBUG_ANIM) Slog.v(TAG, "Stepped animation in " + this + ": more=" + more
- + ", xform=" + mTransformation);
- return more;
+ mWin.cancelAnimation();
+ mWin.destroySurfaceUnchecked();
}
- // This must be called while inside a transaction. Returns true if
- // there is more animation to run.
- boolean stepAnimationLocked(long currentTime) {
- // Save the animation state as it was before this step so WindowManagerService can tell if
- // we just started or just stopped animating by comparing mWasAnimating with isAnimationSet().
- mWasAnimating = mAnimating;
- final DisplayContent displayContent = mWin.getDisplayContent();
- if (mWin.mToken.okToAnimate()) {
- // We will run animations as long as the display isn't frozen.
-
- if (mWin.isDrawnLw() && mAnimation != null) {
- mHasTransformation = true;
- mHasLocalTransformation = true;
- if (!mLocalAnimating) {
- if (DEBUG_ANIM) Slog.v(
- TAG, "Starting animation in " + this +
- " @ " + currentTime + ": ww=" + mWin.mFrame.width() +
- " wh=" + mWin.mFrame.height() +
- " dx=" + mAnimDx + " dy=" + mAnimDy +
- " scale=" + mService.getWindowAnimationScaleLocked());
- final DisplayInfo displayInfo = displayContent.getDisplayInfo();
- if (mAnimateMove) {
- mAnimateMove = false;
- mAnimation.initialize(mWin.mFrame.width(), mWin.mFrame.height(),
- mAnimDx, mAnimDy);
- } else {
- mAnimation.initialize(mWin.mFrame.width(), mWin.mFrame.height(),
- displayInfo.appWidth, displayInfo.appHeight);
- }
- mAnimDx = displayInfo.appWidth;
- mAnimDy = displayInfo.appHeight;
- mAnimation.setStartTime(mAnimationStartTime != -1
- ? mAnimationStartTime
- : currentTime);
- mLocalAnimating = true;
- mAnimating = true;
- }
- if ((mAnimation != null) && mLocalAnimating) {
- mLastAnimationTime = currentTime;
- if (stepAnimation(currentTime)) {
- return true;
- }
- }
- if (DEBUG_ANIM) Slog.v(
- TAG, "Finished animation in " + this +
- " @ " + currentTime);
- //WindowManagerService.this.dump();
- }
- mHasLocalTransformation = false;
- if ((!mLocalAnimating || mAnimationIsEntrance) && mAppAnimator != null
- && mAppAnimator.animation != null) {
- // When our app token is animating, we kind-of pretend like
- // we are as well. Note the mLocalAnimating mAnimationIsEntrance
- // part of this check means that we will only do this if
- // our window is not currently exiting, or it is not
- // locally animating itself. The idea being that one that
- // is exiting and doing a local animation should be removed
- // once that animation is done.
- mAnimating = true;
- mHasTransformation = true;
- mTransformation.clear();
- return false;
- } else if (mHasTransformation) {
- // Little trick to get through the path below to act like
- // we have finished an animation.
- mAnimating = true;
- } else if (isAnimationSet()) {
- mAnimating = true;
- }
- } else if (mAnimation != null) {
- // If the display is frozen, and there is a pending animation,
- // clear it and make sure we run the cleanup code.
- mAnimating = true;
- }
-
- if (!mAnimating && !mLocalAnimating) {
- return false;
- }
-
+ void onAnimationFinished() {
// Done animating, clean up.
if (DEBUG_ANIM) Slog.v(
- TAG, "Animation done in " + this + ": exiting=" + mWin.mAnimatingExit
- + ", reportedVisible="
- + (mWin.mAppToken != null ? mWin.mAppToken.reportedVisible : false));
+ TAG, "Animation done in " + this + ": exiting=" + mWin.mAnimatingExit
+ + ", reportedVisible="
+ + (mWin.mAppToken != null ? mWin.mAppToken.reportedVisible : false));
- mAnimating = false;
- mLocalAnimating = false;
- if (mAnimation != null) {
- mAnimation.cancel();
- mAnimation = null;
- }
if (mAnimator.mWindowDetachedWallpaper == mWin) {
mAnimator.mWindowDetachedWallpaper = null;
}
- mAnimLayer = mWin.getSpecialWindowAnimLayerAdjustment();
- if (DEBUG_LAYERS) Slog.v(TAG, "Stepping win " + this + " anim layer: " + mAnimLayer);
- mHasTransformation = false;
- mHasLocalTransformation = false;
- mStackClip = STACK_CLIP_BEFORE_ANIM;
+
mWin.checkPolicyVisibilityChange();
- mTransformation.clear();
+ final DisplayContent displayContent = mWin.getDisplayContent();
if (mAttrType == LayoutParams.TYPE_STATUS_BAR && mWin.mPolicyVisibility) {
// Upon completion of a not-visible to visible status bar animation a relayout is
// required.
@@ -475,7 +277,11 @@ class WindowStateAnimator {
mWin.onExitAnimationDone();
final int displayId = mWin.getDisplayId();
- mAnimator.setPendingLayoutChanges(displayId, WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM);
+ int pendingLayoutChanges = FINISH_LAYOUT_REDO_ANIM;
+ if (displayContent.mWallpaperController.isWallpaperTarget(mWin)) {
+ pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
+ }
+ mAnimator.setPendingLayoutChanges(displayId, pendingLayoutChanges);
if (DEBUG_LAYOUT_REPEATS)
mService.mWindowPlacerLocked.debugLayoutRepeats(
"WindowStateAnimator", mAnimator.getPendingLayoutChanges(displayId));
@@ -483,8 +289,6 @@ class WindowStateAnimator {
if (mWin.mAppToken != null) {
mWin.mAppToken.updateReportedVisibilityLocked();
}
-
- return false;
}
void hide(String reason) {
@@ -602,13 +406,6 @@ class WindowStateAnimator {
return mWin.getDisplayContent().getDisplay().getLayerStack();
}
- void updateLayerStackInTransaction() {
- if (mSurfaceController != null) {
- mSurfaceController.setLayerStackInTransaction(
- getLayerStack());
- }
- }
-
void resetDrawState() {
mDrawState = DRAW_PENDING;
@@ -616,7 +413,7 @@ class WindowStateAnimator {
return;
}
- if (mWin.mAppToken.mAppAnimator.animation == null) {
+ if (!mWin.mAppToken.isSelfAnimating()) {
mWin.mAppToken.clearAllDrawn();
} else {
// Currently animating, persist current state of allDrawn until animation
@@ -690,8 +487,8 @@ class WindowStateAnimator {
}
mSurfaceController = new WindowSurfaceController(mSession.mSurfaceSession,
- attrs.getTitle().toString(),
- width, height, format, flags, this, windowType, ownerUid);
+ attrs.getTitle().toString(), width, height, format, flags, this,
+ windowType, ownerUid);
mSurfaceFormat = format;
w.setHasSurface(true);
@@ -727,16 +524,6 @@ class WindowStateAnimator {
+ width + "x" + height + "), layer=" + mAnimLayer + " HIDE", false);
}
- // Start a new transaction and apply position & offset.
-
- mService.openSurfaceTransaction();
- try {
- mSurfaceController.setPositionInTransaction(mTmpSize.left, mTmpSize.top, false);
- mSurfaceController.setLayerStackInTransaction(getLayerStack());
- } finally {
- mService.closeSurfaceTransaction("createSurfaceLocked");
- }
-
mLastHidden = true;
if (WindowManagerService.localLOGV) Slog.v(TAG, "Created surface " + this);
@@ -870,35 +657,6 @@ class WindowStateAnimator {
}
void computeShownFrameLocked() {
- final boolean selfTransformation = mHasLocalTransformation;
- Transformation appTransformation = (mAppAnimator != null && mAppAnimator.hasTransformation)
- ? mAppAnimator.transformation : null;
- Transformation wallpaperTargetTransformation = null;
-
- // Wallpapers are animated based on the "real" window they
- // are currently targeting.
- final WindowState wallpaperTarget = mWallpaperControllerLocked.getWallpaperTarget();
- if (mIsWallpaper && wallpaperTarget != null && mService.mAnimateWallpaperWithTarget) {
- final WindowStateAnimator wallpaperAnimator = wallpaperTarget.mWinAnimator;
- if (wallpaperAnimator.mHasLocalTransformation &&
- wallpaperAnimator.mAnimation != null &&
- !wallpaperAnimator.mAnimation.getDetachWallpaper()) {
- wallpaperTargetTransformation = wallpaperAnimator.mTransformation;
- if (DEBUG_WALLPAPER && wallpaperTargetTransformation != null) {
- Slog.v(TAG, "WP target attached xform: " + wallpaperTargetTransformation);
- }
- }
- final AppWindowAnimator wpAppAnimator = wallpaperTarget.mAppToken == null ?
- null : wallpaperTarget.mAppToken.mAppAnimator;
- if (wpAppAnimator != null && wpAppAnimator.hasTransformation
- && wpAppAnimator.animation != null
- && !wpAppAnimator.animation.getDetachWallpaper()) {
- appTransformation = wpAppAnimator.transformation;
- if (DEBUG_WALLPAPER && appTransformation != null) {
- Slog.v(TAG, "WP target app xform: " + appTransformation);
- }
- }
- }
final int displayId = mWin.getDisplayId();
final ScreenRotationAnimation screenRotationAnimation =
@@ -907,15 +665,14 @@ class WindowStateAnimator {
screenRotationAnimation != null && screenRotationAnimation.isAnimating();
mHasClipRect = false;
- if (selfTransformation || wallpaperTargetTransformation != null
- || appTransformation != null || screenAnimation) {
+ if (screenAnimation) {
// cache often used attributes locally
final Rect frame = mWin.mFrame;
final float tmpFloats[] = mService.mTmpFloats;
final Matrix tmpMatrix = mWin.mTmpMatrix;
// Compute the desired transformation.
- if (screenAnimation && screenRotationAnimation.isRotating()) {
+ if (screenRotationAnimation.isRotating()) {
// If we are doing a screen animation, the global rotation
// applied to windows can result in windows that are carefully
// aligned with each other to slightly separate, allowing you
@@ -933,34 +690,14 @@ class WindowStateAnimator {
} else {
tmpMatrix.reset();
}
- tmpMatrix.postScale(mWin.mGlobalScale, mWin.mGlobalScale);
- if (selfTransformation) {
- tmpMatrix.postConcat(mTransformation.getMatrix());
- }
- if (wallpaperTargetTransformation != null) {
- tmpMatrix.postConcat(wallpaperTargetTransformation.getMatrix());
- }
- if (appTransformation != null) {
- tmpMatrix.postConcat(appTransformation.getMatrix());
- }
+ tmpMatrix.postScale(mWin.mGlobalScale, mWin.mGlobalScale);
- int left = frame.left;
- int top = frame.top;
- if (mWin.isChildWindow()) {
- WindowState parent = mWin.getParentWindow();
- left -= parent.mFrame.left;
- top -= parent.mFrame.top;
- }
+ // WindowState.prepareSurfaces expands for surface insets (in order they don't get
+ // clipped by the WindowState surface), so we need to go into the other direction here.
+ tmpMatrix.postTranslate(mWin.mXOffset + mWin.mAttrs.surfaceInsets.left,
+ mWin.mYOffset + mWin.mAttrs.surfaceInsets.top);
- // The translation that applies the position of the window needs to be applied at the
- // end in case that other translations include scaling. Otherwise the scaling will
- // affect this translation. But it needs to be set before the screen rotation animation
- // so the pivot point is at the center of the screen for all windows.
- tmpMatrix.postTranslate(left + mWin.mXOffset, top + mWin.mYOffset);
- if (screenAnimation) {
- tmpMatrix.postConcat(screenRotationAnimation.getEnterTransformation().getMatrix());
- }
// "convert" it into SurfaceFlinger's format
// (a 2x2 matrix + an offset)
@@ -989,30 +726,6 @@ class WindowStateAnimator {
|| (mWin.isIdentityMatrix(mDsDx, mDtDx, mDtDy, mDsDy)
&& x == frame.left && y == frame.top))) {
//Slog.i(TAG_WM, "Applying alpha transform");
- if (selfTransformation) {
- mShownAlpha *= mTransformation.getAlpha();
- }
- if (wallpaperTargetTransformation != null) {
- mShownAlpha *= wallpaperTargetTransformation.getAlpha();
- }
- if (appTransformation != null) {
- mShownAlpha *= appTransformation.getAlpha();
- if (appTransformation.hasClipRect()) {
- mClipRect.set(appTransformation.getClipRect());
- mHasClipRect = true;
- // The app transformation clip will be in the coordinate space of the main
- // activity window, which the animation correctly assumes will be placed at
- // (0,0)+(insets) relative to the containing frame. This isn't necessarily
- // true for child windows though which can have an arbitrary frame position
- // relative to their containing frame. We need to offset the difference
- // between the containing frame as used to calculate the crop and our
- // bounds to compensate for this.
- if (mWin.layoutInParentFrame()) {
- mClipRect.offset( (mWin.mContainingFrame.left - mWin.mFrame.left),
- mWin.mContainingFrame.top - mWin.mFrame.top );
- }
- }
- }
if (screenAnimation) {
mShownAlpha *= screenRotationAnimation.getEnterTransformation().getAlpha();
}
@@ -1023,10 +736,6 @@ class WindowStateAnimator {
if ((DEBUG_ANIM || WindowManagerService.localLOGV)
&& (mShownAlpha == 1.0 || mShownAlpha == 0.0)) Slog.v(
TAG, "computeShownFrameLocked: Animating " + this + " mAlpha=" + mAlpha
- + " self=" + (selfTransformation ? mTransformation.getAlpha() : "null")
- + " attached=" + (wallpaperTargetTransformation == null ?
- "null" : wallpaperTargetTransformation.getAlpha())
- + " app=" + (appTransformation == null ? "null" : appTransformation.getAlpha())
+ " screen=" + (screenAnimation ?
screenRotationAnimation.getEnterTransformation().getAlpha() : "null"));
return;
@@ -1045,10 +754,10 @@ class WindowStateAnimator {
TAG, "computeShownFrameLocked: " + this +
" not attached, mAlpha=" + mAlpha);
- mWin.mShownPosition.set(mWin.mFrame.left, mWin.mFrame.top);
- if (mWin.mXOffset != 0 || mWin.mYOffset != 0) {
- mWin.mShownPosition.offset(mWin.mXOffset, mWin.mYOffset);
- }
+ // WindowState.prepareSurfaces expands for surface insets (in order they don't get
+ // clipped by the WindowState surface), so we need to go into the other direction here.
+ mWin.mShownPosition.set(mWin.mXOffset + mWin.mAttrs.surfaceInsets.left,
+ mWin.mYOffset + mWin.mAttrs.surfaceInsets.top);
mShownAlpha = mAlpha;
mHaveMatrix = false;
mDsDx = mWin.mGlobalScale;
@@ -1058,47 +767,6 @@ class WindowStateAnimator {
}
/**
- * In some scenarios we use a screen space clip rect (so called, final clip rect)
- * to crop to stack bounds. Generally because it's easier to deal with while
- * animating.
- *
- * @return True in scenarios where we use the final clip rect for stack clipping.
- */
- private boolean useFinalClipRect() {
- return (isAnimationSet() && resolveStackClip() == STACK_CLIP_AFTER_ANIM)
- || mDestroyPreservedSurfaceUponRedraw || mWin.inPinnedWindowingMode();
- }
-
- /**
- * Calculate the screen-space crop rect and fill finalClipRect.
- * @return true if finalClipRect has been filled, otherwise,
- * no screen space crop should be applied.
- */
- private boolean calculateFinalCrop(Rect finalClipRect) {
- final WindowState w = mWin;
- final DisplayContent displayContent = w.getDisplayContent();
- finalClipRect.setEmpty();
-
- if (displayContent == null) {
- return false;
- }
-
- if (!shouldCropToStackBounds() || !useFinalClipRect()) {
- return false;
- }
-
- // Task is non-null per shouldCropToStackBounds
- final TaskStack stack = w.getTask().mStack;
- stack.getDimBounds(finalClipRect);
-
- if (stack.getWindowConfiguration().tasksAreFloating()) {
- w.expandForSurfaceInsets(finalClipRect);
- }
-
- return true;
- }
-
- /**
* Calculate the window-space crop rect and fill clipRect.
* @return true if clipRect has been filled otherwise, no window space crop should be applied.
*/
@@ -1158,9 +826,6 @@ class WindowStateAnimator {
// so we need to translate to match the actual surface coordinates.
clipRect.offset(w.mAttrs.surfaceInsets.left, w.mAttrs.surfaceInsets.top);
- if (!useFinalClipRect()) {
- adjustCropToStackBounds(clipRect, isFreeformResizing);
- }
if (DEBUG_WINDOW_CROP) Slog.d(TAG,
"win=" + w + " Clip rect after stack adjustment=" + clipRect);
@@ -1169,9 +834,9 @@ class WindowStateAnimator {
return true;
}
- private void applyCrop(Rect clipRect, Rect finalClipRect, boolean recoveringMemory) {
+ private void applyCrop(Rect clipRect, boolean recoveringMemory) {
if (DEBUG_WINDOW_CROP) Slog.d(TAG, "applyCrop: win=" + mWin
- + " clipRect=" + clipRect + " finalClipRect=" + finalClipRect);
+ + " clipRect=" + clipRect);
if (clipRect != null) {
if (!clipRect.equals(mLastClipRect)) {
mLastClipRect.set(clipRect);
@@ -1180,93 +845,6 @@ class WindowStateAnimator {
} else {
mSurfaceController.clearCropInTransaction(recoveringMemory);
}
-
- if (finalClipRect == null) {
- finalClipRect = mService.mTmpRect;
- finalClipRect.setEmpty();
- }
- if (!finalClipRect.equals(mLastFinalClipRect)) {
- mLastFinalClipRect.set(finalClipRect);
- mSurfaceController.setFinalCropInTransaction(finalClipRect);
- if (mDestroyPreservedSurfaceUponRedraw && mPendingDestroySurface != null) {
- mPendingDestroySurface.setFinalCropInTransaction(finalClipRect);
- }
- }
- }
-
- private int resolveStackClip() {
- // App animation overrides window animation stack clip mode.
- if (mAppAnimator != null && mAppAnimator.animation != null) {
- return mAppAnimator.getStackClip();
- } else {
- return mStackClip;
- }
- }
-
- private boolean shouldCropToStackBounds() {
- final WindowState w = mWin;
- final DisplayContent displayContent = w.getDisplayContent();
- if (displayContent != null && !displayContent.isDefaultDisplay) {
- // There are some windows that live on other displays while their app and main window
- // live on the default display (e.g. casting...). We don't want to crop this windows
- // to the stack bounds which is only currently supported on the default display.
- // TODO(multi-display): Need to support cropping to stack bounds on other displays
- // when we have stacks on other displays.
- return false;
- }
-
- final Task task = w.getTask();
- if (task == null || !task.cropWindowsToStackBounds()) {
- return false;
- }
-
- final int stackClip = resolveStackClip();
-
- // It's animating and we don't want to clip it to stack bounds during animation - abort.
- if (isAnimationSet() && stackClip == STACK_CLIP_NONE) {
- return false;
- }
- return true;
- }
-
- private void adjustCropToStackBounds(Rect clipRect,
- boolean isFreeformResizing) {
- final WindowState w = mWin;
-
- if (!shouldCropToStackBounds()) {
- return;
- }
-
- final TaskStack stack = w.getTask().mStack;
- stack.getDimBounds(mTmpStackBounds);
- final Rect surfaceInsets = w.getAttrs().surfaceInsets;
- // When we resize we use the big surface approach, which means we can't trust the
- // window frame bounds anymore. Instead, the window will be placed at 0, 0, but to avoid
- // hardcoding it, we use surface coordinates.
- final int frameX = isFreeformResizing ? (int) mSurfaceController.getX() :
- w.mFrame.left + mWin.mXOffset - surfaceInsets.left;
- final int frameY = isFreeformResizing ? (int) mSurfaceController.getY() :
- w.mFrame.top + mWin.mYOffset - surfaceInsets.top;
-
- // We need to do some acrobatics with surface position, because their clip region is
- // relative to the inside of the surface, but the stack bounds aren't.
- final WindowConfiguration winConfig = w.getWindowConfiguration();
- if (winConfig.hasWindowShadow() && !winConfig.canResizeTask()) {
- // The windows in this stack display drop shadows and the fill the entire stack
- // area. Adjust the stack bounds we will use to cropping take into account the
- // offsets we use to display the drop shadow so it doesn't get cropped.
- mTmpStackBounds.inset(-surfaceInsets.left, -surfaceInsets.top,
- -surfaceInsets.right, -surfaceInsets.bottom);
- }
-
- clipRect.left = Math.max(0,
- Math.max(mTmpStackBounds.left, frameX + clipRect.left) - frameX);
- clipRect.top = Math.max(0,
- Math.max(mTmpStackBounds.top, frameY + clipRect.top) - frameY);
- clipRect.right = Math.max(0,
- Math.min(mTmpStackBounds.right, frameX + clipRect.right) - frameX);
- clipRect.bottom = Math.max(0,
- Math.min(mTmpStackBounds.bottom, frameY + clipRect.bottom) - frameY);
}
void setSurfaceBoundariesLocked(final boolean recoveringMemory) {
@@ -1311,13 +889,10 @@ class WindowStateAnimator {
// updates until a resize occurs.
mService.markForSeamlessRotation(w, w.mSeamlesslyRotated && !mSurfaceResized);
- Rect clipRect = null, finalClipRect = null;
+ Rect clipRect = null;
if (calculateCrop(mTmpClipRect)) {
clipRect = mTmpClipRect;
}
- if (calculateFinalCrop(mTmpFinalClipRect)) {
- finalClipRect = mTmpFinalClipRect;
- }
float surfaceWidth = mSurfaceController.getWidth();
float surfaceHeight = mSurfaceController.getHeight();
@@ -1382,7 +957,6 @@ class WindowStateAnimator {
// Always clip to the stack bounds since the surface can be larger with the current
// scale
clipRect = null;
- finalClipRect = mTmpStackBounds;
} else {
// We want to calculate the scaling based on the content area, not based on
// the entire surface, so that we scale in sync with windows that don't have insets.
@@ -1393,7 +967,6 @@ class WindowStateAnimator {
// expose the whole window in buffer space, and not risk extending
// past where the system would have cropped us
clipRect = null;
- finalClipRect = null;
}
// In the case of ForceScaleToStack we scale entire tasks together,
@@ -1441,7 +1014,7 @@ class WindowStateAnimator {
}
if (!w.mSeamlesslyRotated) {
- applyCrop(clipRect, finalClipRect, recoveringMemory);
+ applyCrop(clipRect, recoveringMemory);
mSurfaceController.setMatrixInTransaction(mDsDx * w.mHScale * mExtraHScale,
mDtDx * w.mVScale * mExtraVScale,
mDtDy * w.mHScale * mExtraHScale,
@@ -1451,7 +1024,7 @@ class WindowStateAnimator {
if (mSurfaceResized) {
mReportSurfaceResized = true;
mAnimator.setPendingLayoutChanges(w.getDisplayId(),
- WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER);
+ FINISH_LAYOUT_REDO_WALLPAPER);
}
}
@@ -1563,7 +1136,7 @@ class WindowStateAnimator {
// Run another pass through performLayout to set mHasContent in the
// LogicalDisplay.
mAnimator.setPendingLayoutChanges(w.getDisplayId(),
- WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM);
+ FINISH_LAYOUT_REDO_ANIM);
} else {
w.setOrientationChanging(false);
}
@@ -1639,7 +1212,7 @@ class WindowStateAnimator {
mService.openSurfaceTransaction();
mSurfaceController.setPositionInTransaction(mWin.mFrame.left + left,
mWin.mFrame.top + top, false);
- applyCrop(null, null, false);
+ applyCrop(null, false);
} catch (RuntimeException e) {
Slog.w(TAG, "Error positioning surface of " + mWin
+ " pos=(" + left + "," + top + ")", e);
@@ -1737,12 +1310,18 @@ class WindowStateAnimator {
* @return true if an animation has been loaded.
*/
boolean applyAnimationLocked(int transit, boolean isEntrance) {
- if (mLocalAnimating && mAnimationIsEntrance == isEntrance) {
+ if (mWin.isSelfAnimating() && mAnimationIsEntrance == isEntrance) {
// If we are trying to apply an animation, but already running
// an animation of the same type, then just leave that one alone.
return true;
}
+ if (isEntrance && mWin.mAttrs.type == TYPE_INPUT_METHOD) {
+ mWin.getDisplayContent().adjustForImeIfNeeded();
+ mWin.setDisplayLayoutNeeded();
+ mService.mWindowPlacerLocked.requestTraversal();
+ }
+
// Only apply an animation if the display isn't frozen. If it is
// frozen, there is no reason to animate and it can cause strange
// artifacts when we unfreeze the display if some different animation
@@ -1781,44 +1360,19 @@ class WindowStateAnimator {
+ " isEntrance=" + isEntrance + " Callers " + Debug.getCallers(3));
if (a != null) {
if (DEBUG_ANIM) logWithStack(TAG, "Loaded animation " + a + " for " + this);
- setAnimation(a);
+ mWin.startAnimation(a);
mAnimationIsEntrance = isEntrance;
}
} else {
- clearAnimation();
+ mWin.cancelAnimation();
}
- Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
- if (mWin.mAttrs.type == TYPE_INPUT_METHOD) {
+ if (!isEntrance && mWin.mAttrs.type == TYPE_INPUT_METHOD) {
mWin.getDisplayContent().adjustForImeIfNeeded();
- if (isEntrance) {
- mWin.setDisplayLayoutNeeded();
- mService.mWindowPlacerLocked.requestTraversal();
- }
}
- return mAnimation != null;
- }
- private void applyFadeoutDuringKeyguardExitAnimation() {
- long startTime = mAnimation.getStartTime();
- long duration = mAnimation.getDuration();
- long elapsed = mLastAnimationTime - startTime;
- long fadeDuration = duration - elapsed;
- if (fadeDuration <= 0) {
- // Never mind, this would be no visible animation, so abort the animation change.
- return;
- }
- AnimationSet newAnimation = new AnimationSet(false /* shareInterpolator */);
- newAnimation.setDuration(duration);
- newAnimation.setStartTime(startTime);
- newAnimation.addAnimation(mAnimation);
- Animation fadeOut = AnimationUtils.loadAnimation(
- mContext, com.android.internal.R.anim.app_starting_exit);
- fadeOut.setDuration(fadeDuration);
- fadeOut.setStartOffset(elapsed);
- newAnimation.addAnimation(fadeOut);
- newAnimation.initialize(mWin.mFrame.width(), mWin.mFrame.height(), mAnimDx, mAnimDy);
- mAnimation = newAnimation;
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+ return isAnimationSet();
}
void writeToProto(ProtoOutputStream proto, long fieldId) {
@@ -1831,20 +1385,8 @@ class WindowStateAnimator {
}
public void dump(PrintWriter pw, String prefix, boolean dumpAll) {
- if (mAnimating || mLocalAnimating || mAnimationIsEntrance
- || mAnimation != null) {
- pw.print(prefix); pw.print("mAnimating="); pw.print(mAnimating);
- pw.print(" mLocalAnimating="); pw.print(mLocalAnimating);
- pw.print(" mAnimationIsEntrance="); pw.print(mAnimationIsEntrance);
- pw.print(" mAnimation="); pw.print(mAnimation);
- pw.print(" mStackClip="); pw.println(mStackClip);
- }
- if (mHasTransformation || mHasLocalTransformation) {
- pw.print(prefix); pw.print("XForm: has=");
- pw.print(mHasTransformation);
- pw.print(" hasLocal="); pw.print(mHasLocalTransformation);
- pw.print(" "); mTransformation.printShortString(pw);
- pw.println();
+ if (mAnimationIsEntrance) {
+ pw.print(prefix); pw.print(" mAnimationIsEntrance="); pw.print(mAnimationIsEntrance);
}
if (mSurfaceController != null) {
mSurfaceController.dump(pw, prefix, dumpAll);
@@ -1924,44 +1466,6 @@ class WindowStateAnimator {
}
}
- void setMoveAnimation(int left, int top) {
- final Animation a = AnimationUtils.loadAnimation(mContext,
- com.android.internal.R.anim.window_move_from_decor);
- setAnimation(a);
- mAnimDx = mWin.mLastFrame.left - left;
- mAnimDy = mWin.mLastFrame.top - top;
- mAnimateMove = true;
- }
-
- void deferTransactionUntilParentFrame(long frameNumber) {
- if (!mWin.isChildWindow()) {
- return;
- }
- mSurfaceController.deferTransactionUntil(
- mWin.getParentWindow().mWinAnimator.mSurfaceController.getHandle(), frameNumber);
- }
-
- /**
- * Sometimes we need to synchronize the first frame of animation with some external event.
- * To achieve this, we prolong the start of the animation and keep producing the first frame of
- * the animation.
- */
- private long getAnimationFrameTime(Animation animation, long currentTime) {
- if (mAnimationStartDelayed) {
- animation.setStartTime(currentTime);
- return currentTime + 1;
- }
- return currentTime;
- }
-
- void startDelayingAnimationStart() {
- mAnimationStartDelayed = true;
- }
-
- void endDelayingAnimationStart() {
- mAnimationStartDelayed = false;
- }
-
void seamlesslyRotateWindow(int oldRotation, int newRotation) {
final WindowState w = mWin;
if (!w.isVisibleNow() || w.mIsWallpaper) {
@@ -2043,4 +1547,8 @@ class WindowStateAnimator {
mSurfaceController.detachChildren();
}
}
+
+ int getLayer() {
+ return mLastLayer;
+ }
}
diff --git a/com/android/server/wm/WindowSurfaceController.java b/com/android/server/wm/WindowSurfaceController.java
index 6746754b..e26a362b 100644
--- a/com/android/server/wm/WindowSurfaceController.java
+++ b/com/android/server/wm/WindowSurfaceController.java
@@ -59,8 +59,8 @@ class WindowSurfaceController {
private boolean mSurfaceShown = false;
private float mSurfaceX = 0;
private float mSurfaceY = 0;
- private float mSurfaceW = 0;
- private float mSurfaceH = 0;
+ private int mSurfaceW = 0;
+ private int mSurfaceH = 0;
// Initialize to the identity matrix.
private float mLastDsdx = 1;
@@ -517,11 +517,11 @@ class WindowSurfaceController {
return mSurfaceY;
}
- float getWidth() {
+ int getWidth() {
return mSurfaceW;
}
- float getHeight() {
+ int getHeight() {
return mSurfaceH;
}
diff --git a/com/android/server/wm/WindowSurfacePlacer.java b/com/android/server/wm/WindowSurfacePlacer.java
index 169d0a3a..bdab9c76 100644
--- a/com/android/server/wm/WindowSurfacePlacer.java
+++ b/com/android/server/wm/WindowSurfacePlacer.java
@@ -97,9 +97,6 @@ class WindowSurfacePlacer {
static final int SET_TURN_ON_SCREEN = 1 << 4;
static final int SET_WALLPAPER_ACTION_PENDING = 1 << 5;
- private final Rect mTmpStartRect = new Rect();
- private final Rect mTmpContentRect = new Rect();
-
private boolean mTraversalScheduled;
private int mDeferDepth = 0;
@@ -361,14 +358,9 @@ class WindowSurfacePlacer {
mService.mAppTransition.setLastAppTransition(transit, topOpeningApp, topClosingApp);
- final AppWindowAnimator openingAppAnimator = (topOpeningApp == null) ? null :
- topOpeningApp.mAppAnimator;
- final AppWindowAnimator closingAppAnimator = (topClosingApp == null) ? null :
- topClosingApp.mAppAnimator;
-
final int flags = mService.mAppTransition.getTransitFlags();
- int layoutRedo = mService.mAppTransition.goodToGo(transit, openingAppAnimator,
- closingAppAnimator, mService.mOpeningApps, mService.mClosingApps);
+ int layoutRedo = mService.mAppTransition.goodToGo(transit, topOpeningApp,
+ topClosingApp, mService.mOpeningApps, mService.mClosingApps);
handleNonAppWindowsInTransition(transit, flags);
mService.mAppTransition.postAnimationCallback();
mService.mAppTransition.clear();
@@ -405,14 +397,8 @@ class WindowSurfacePlacer {
final int appsCount = mService.mOpeningApps.size();
for (int i = 0; i < appsCount; i++) {
AppWindowToken wtoken = mService.mOpeningApps.valueAt(i);
- final AppWindowAnimator appAnimator = wtoken.mAppAnimator;
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG, "Now opening app" + wtoken);
- if (!appAnimator.usingTransferredAnimation) {
- appAnimator.clearThumbnail();
- appAnimator.setNullAnimation();
- }
-
if (!wtoken.setVisibility(animLp, true, transit, false, voiceInteraction)){
// This token isn't going to be animating. Add it to the list of tokens to
// be notified of app transition complete since the notification will not be
@@ -421,19 +407,17 @@ class WindowSurfacePlacer {
}
wtoken.updateReportedVisibilityLocked();
wtoken.waitingToShow = false;
- wtoken.setAllAppWinAnimators();
if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
">>> OPEN TRANSACTION handleAppTransitionReadyLocked()");
mService.openSurfaceTransaction();
try {
- mService.mAnimator.orAnimating(appAnimator.showAllWindowsLocked());
+ wtoken.showAllWindowsLocked();
} finally {
mService.closeSurfaceTransaction("handleAppTransitionReadyLocked");
if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
"<<< CLOSE TRANSACTION handleAppTransitionReadyLocked()");
}
- mService.mAnimator.mAppWindowAnimating |= appAnimator.isAnimating();
if (animLp != null) {
final int layer = wtoken.getHighestAnimLayer();
@@ -443,7 +427,7 @@ class WindowSurfacePlacer {
}
}
if (mService.mAppTransition.isNextAppTransitionThumbnailUp()) {
- createThumbnailAppAnimator(transit, wtoken);
+ wtoken.attachThumbnailAnimation();
}
}
return topOpeningApp;
@@ -456,17 +440,11 @@ class WindowSurfacePlacer {
for (int i = 0; i < appsCount; i++) {
AppWindowToken wtoken = mService.mClosingApps.valueAt(i);
- final AppWindowAnimator appAnimator = wtoken.mAppAnimator;
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG, "Now closing app " + wtoken);
- appAnimator.clearThumbnail();
- appAnimator.setNullAnimation();
// TODO: Do we need to add to mNoAnimationNotifyOnTransitionFinished like above if not
// animating?
wtoken.setVisibility(animLp, false, transit, false, voiceInteraction);
wtoken.updateReportedVisibilityLocked();
- // setAllAppWinAnimators so the windows get onExitAnimationDone once the animation is
- // done.
- wtoken.setAllAppWinAnimators();
// Force the allDrawn flag, because we want to start
// this guy's animations regardless of whether it's
// gotten drawn.
@@ -478,7 +456,6 @@ class WindowSurfacePlacer {
&& wtoken.getController() != null) {
wtoken.getController().removeStartingWindow();
}
- mService.mAnimator.mAppWindowAnimating |= appAnimator.isAnimating();
if (animLp != null) {
int layer = wtoken.getHighestAnimLayer();
@@ -488,7 +465,7 @@ class WindowSurfacePlacer {
}
}
if (mService.mAppTransition.isNextAppTransitionThumbnailDown()) {
- createThumbnailAppAnimator(transit, wtoken);
+ wtoken.attachThumbnailAnimation();
}
}
}
@@ -606,7 +583,7 @@ class WindowSurfacePlacer {
+ ", oldWallpaper=" + oldWallpaper
+ ", openingApps=" + openingApps
+ ", closingApps=" + closingApps);
- mService.mAnimateWallpaperWithTarget = false;
+
if (openingCanBeWallpaperTarget && transit == TRANSIT_KEYGUARD_GOING_AWAY) {
transit = TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER;
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG,
@@ -645,8 +622,6 @@ class WindowSurfacePlacer {
transit = TRANSIT_WALLPAPER_OPEN;
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG, "New transit into wallpaper: "
+ AppTransition.appTransitionToString(transit));
- } else {
- mService.mAnimateWallpaperWithTarget = true;
}
}
return transit;
@@ -667,99 +642,13 @@ class WindowSurfacePlacer {
final WindowState win = mService.getDefaultDisplayContentLocked().findFocusedWindow();
if (win != null) {
final AppWindowToken wtoken = win.mAppToken;
- final AppWindowAnimator appAnimator = wtoken.mAppAnimator;
if (DEBUG_APP_TRANSITIONS)
Slog.v(TAG, "Now animating app in place " + wtoken);
- appAnimator.clearThumbnail();
- appAnimator.setNullAnimation();
- mService.updateTokenInPlaceLocked(wtoken, transit);
+ wtoken.cancelAnimation();
+ wtoken.applyAnimationLocked(null, transit, false, false);
wtoken.updateReportedVisibilityLocked();
- wtoken.setAllAppWinAnimators();
- mService.mAnimator.mAppWindowAnimating |= appAnimator.isAnimating();
- mService.mAnimator.orAnimating(appAnimator.showAllWindowsLocked());
- }
- }
- }
-
- private void createThumbnailAppAnimator(int transit, AppWindowToken appToken) {
- AppWindowAnimator openingAppAnimator = (appToken == null) ? null : appToken.mAppAnimator;
- if (openingAppAnimator == null || openingAppAnimator.animation == null) {
- return;
- }
- final int taskId = appToken.getTask().mTaskId;
- final GraphicBuffer thumbnailHeader =
- mService.mAppTransition.getAppTransitionThumbnailHeader(taskId);
- if (thumbnailHeader == null) {
- if (DEBUG_APP_TRANSITIONS) Slog.d(TAG, "No thumbnail header bitmap for: " + taskId);
- return;
- }
- // This thumbnail animation is very special, we need to have
- // an extra surface with the thumbnail included with the animation.
- Rect dirty = new Rect(0, 0, thumbnailHeader.getWidth(), thumbnailHeader.getHeight());
- try {
- // TODO(multi-display): support other displays
- final DisplayContent displayContent = mService.getDefaultDisplayContentLocked();
- final Display display = displayContent.getDisplay();
- final DisplayInfo displayInfo = displayContent.getDisplayInfo();
-
- // Create a new surface for the thumbnail
- WindowState window = appToken.findMainWindow();
- final SurfaceControl surfaceControl = appToken.makeSurface()
- .setName("thumbnail anim")
- .setSize(dirty.width(), dirty.height())
- .setFormat(PixelFormat.TRANSLUCENT)
- .setMetadata(appToken.windowType,
- window != null ? window.mOwnerUid : Binder.getCallingUid())
- .build();
-
- if (SHOW_TRANSACTIONS) {
- Slog.i(TAG, " THUMBNAIL " + surfaceControl + ": CREATE");
- }
-
- // Transfer the thumbnail to the surface
- Surface drawSurface = new Surface();
- drawSurface.copyFrom(surfaceControl);
- drawSurface.attachAndQueueBuffer(thumbnailHeader);
- drawSurface.release();
-
- // Get the thumbnail animation
- Animation anim;
- if (mService.mAppTransition.isNextThumbnailTransitionAspectScaled()) {
- // If this is a multi-window scenario, we use the windows frame as
- // destination of the thumbnail header animation. If this is a full screen
- // window scenario, we use the whole display as the target.
- WindowState win = appToken.findMainWindow();
- Rect appRect = win != null ? win.getContentFrameLw() :
- new Rect(0, 0, displayInfo.appWidth, displayInfo.appHeight);
- Rect insets = win != null ? win.mContentInsets : null;
- final Configuration displayConfig = displayContent.getConfiguration();
- // For the new aspect-scaled transition, we want it to always show
- // above the animating opening/closing window, and we want to
- // synchronize its thumbnail surface with the surface for the
- // open/close animation (only on the way down)
- anim = mService.mAppTransition.createThumbnailAspectScaleAnimationLocked(appRect,
- insets, thumbnailHeader, taskId, displayConfig.uiMode,
- displayConfig.orientation);
- openingAppAnimator.deferThumbnailDestruction =
- !mService.mAppTransition.isNextThumbnailTransitionScaleUp();
- } else {
- anim = mService.mAppTransition.createThumbnailScaleAnimationLocked(
- displayInfo.appWidth, displayInfo.appHeight, transit, thumbnailHeader);
+ wtoken.showAllWindowsLocked();
}
- anim.restrictDuration(MAX_ANIMATION_DURATION);
- anim.scaleCurrentDuration(mService.getTransitionAnimationScaleLocked());
-
- openingAppAnimator.thumbnail = surfaceControl;
- openingAppAnimator.thumbnailAnimation = anim;
- mService.mAppTransition.getNextAppTransitionStartRect(taskId, mTmpStartRect);
-
- // We parent the thumbnail to the app token, and just place it
- // on top of anything else in the app token.
- surfaceControl.setLayer(Integer.MAX_VALUE);
- } catch (Surface.OutOfResourcesException e) {
- Slog.e(TAG, "Can't allocate thumbnail/Canvas surface w="
- + dirty.width() + " h=" + dirty.height(), e);
- openingAppAnimator.clearThumbnail();
}
}
diff --git a/com/android/server/wm/WindowToken.java b/com/android/server/wm/WindowToken.java
index 62a2abbb..bad9bf53 100644
--- a/com/android/server/wm/WindowToken.java
+++ b/com/android/server/wm/WindowToken.java
@@ -46,9 +46,6 @@ import java.io.PrintWriter;
class WindowToken extends WindowContainer<WindowState> {
private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowToken" : TAG_WM;
- // The window manager!
- protected final WindowManagerService mService;
-
// The actual token.
final IBinder token;
@@ -66,7 +63,7 @@ class WindowToken extends WindowContainer<WindowState> {
boolean paused = false;
// Should this token's windows be hidden?
- boolean hidden;
+ private boolean mHidden;
// Temporary for finding which tokens no longer have visible windows.
boolean hasVisible;
@@ -107,7 +104,7 @@ class WindowToken extends WindowContainer<WindowState> {
WindowToken(WindowManagerService service, IBinder _token, int type, boolean persistOnEmpty,
DisplayContent dc, boolean ownerCanManageAppTokens) {
- mService = service;
+ super(service);
token = _token;
windowType = type;
mPersistOnEmpty = persistOnEmpty;
@@ -115,6 +112,16 @@ class WindowToken extends WindowContainer<WindowState> {
onDisplayChanged(dc);
}
+ void setHidden(boolean hidden) {
+ if (hidden != mHidden) {
+ mHidden = hidden;
+ }
+ }
+
+ boolean isHidden() {
+ return mHidden;
+ }
+
void removeAllWindowsIfPossible() {
for (int i = mChildren.size() - 1; i >= 0; --i) {
final WindowState win = mChildren.get(i);
@@ -125,10 +132,15 @@ class WindowToken extends WindowContainer<WindowState> {
}
void setExiting() {
+ if (mChildren.size() == 0) {
+ super.removeImmediately();
+ return;
+ }
+
// This token is exiting, so allow it to be removed when it no longer contains any windows.
mPersistOnEmpty = false;
- if (hidden) {
+ if (mHidden) {
return;
}
@@ -144,7 +156,7 @@ class WindowToken extends WindowContainer<WindowState> {
changed |= win.onSetAppExiting();
}
- hidden = true;
+ setHidden(true);
if (changed) {
mService.mWindowPlacerLocked.performSurfacePlacement();
@@ -187,11 +199,6 @@ class WindowToken extends WindowContainer<WindowState> {
return mChildren.isEmpty();
}
- // Used by AppWindowToken.
- int getAnimLayerAdjustment() {
- return 0;
- }
-
WindowState getReplacingWindow() {
for (int i = mChildren.size() - 1; i >= 0; i--) {
final WindowState win = mChildren.get(i);
@@ -255,12 +262,6 @@ class WindowToken extends WindowContainer<WindowState> {
// up with goodToGo, so we don't move a window
// to another display before the window behind
// it is ready.
- SurfaceControl.openTransaction();
- for (int i = mChildren.size() - 1; i >= 0; --i) {
- final WindowState win = mChildren.get(i);
- win.mWinAnimator.updateLayerStackInTransaction();
- }
- SurfaceControl.closeTransaction();
super.onDisplayChanged(dc);
}
@@ -278,10 +279,11 @@ class WindowToken extends WindowContainer<WindowState> {
proto.end(token);
}
- void dump(PrintWriter pw, String prefix) {
+ void dump(PrintWriter pw, String prefix, boolean dumpAll) {
+ super.dump(pw, prefix, dumpAll);
pw.print(prefix); pw.print("windows="); pw.println(mChildren);
pw.print(prefix); pw.print("windowType="); pw.print(windowType);
- pw.print(" hidden="); pw.print(hidden);
+ pw.print(" hidden="); pw.print(mHidden);
pw.print(" hasVisible="); pw.println(hasVisible);
if (waitingToShow || sendingToBottom) {
pw.print(prefix); pw.print("waitingToShow="); pw.print(waitingToShow);
diff --git a/com/android/settingslib/HelpUtils.java b/com/android/settingslib/HelpUtils.java
index 2c264107..8055caaa 100644
--- a/com/android/settingslib/HelpUtils.java
+++ b/com/android/settingslib/HelpUtils.java
@@ -227,7 +227,7 @@ public class HelpUtils {
// cache the version code
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
- sCachedVersionCode = Integer.toString(info.versionCode);
+ sCachedVersionCode = Long.toString(info.getLongVersionCode());
// append the version code to the uri
builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
diff --git a/com/android/settingslib/RestrictedLockUtils.java b/com/android/settingslib/RestrictedLockUtils.java
index c3a36e97..fce5dd9c 100644
--- a/com/android/settingslib/RestrictedLockUtils.java
+++ b/com/android/settingslib/RestrictedLockUtils.java
@@ -432,53 +432,9 @@ public class RestrictedLockUtils {
* the admin component will be set to {@code null} and userId to {@link UserHandle#USER_NULL}
*/
public static EnforcedAdmin checkIfMaximumTimeToLockIsSet(Context context) {
- final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
- Context.DEVICE_POLICY_SERVICE);
- if (dpm == null) {
- return null;
- }
- EnforcedAdmin enforcedAdmin = null;
- final int userId = UserHandle.myUserId();
- final UserManager um = UserManager.get(context);
- final List<UserInfo> profiles = um.getProfiles(userId);
- final int profilesSize = profiles.size();
- // As we do not have a separate screen lock timeout settings for work challenge,
- // we need to combine all profiles maximum time to lock even work challenge is
- // enabled.
- for (int i = 0; i < profilesSize; i++) {
- final UserInfo userInfo = profiles.get(i);
- final List<ComponentName> admins = dpm.getActiveAdminsAsUser(userInfo.id);
- if (admins == null) {
- continue;
- }
- for (ComponentName admin : admins) {
- if (dpm.getMaximumTimeToLock(admin, userInfo.id) > 0) {
- if (enforcedAdmin == null) {
- enforcedAdmin = new EnforcedAdmin(admin, userInfo.id);
- } else {
- return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
- }
- // This same admins could have set policies both on the managed profile
- // and on the parent. So, if the admin has set the policy on the
- // managed profile here, we don't need to further check if that admin
- // has set policy on the parent admin.
- continue;
- }
- if (userInfo.isManagedProfile()) {
- // If userInfo.id is a managed profile, we also need to look at
- // the policies set on the parent.
- DevicePolicyManager parentDpm = sProxy.getParentProfileInstance(dpm, userInfo);
- if (parentDpm.getMaximumTimeToLock(admin, userInfo.id) > 0) {
- if (enforcedAdmin == null) {
- enforcedAdmin = new EnforcedAdmin(admin, userInfo.id);
- } else {
- return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
- }
- }
- }
- }
- }
- return enforcedAdmin;
+ return checkForLockSetting(context, UserHandle.myUserId(),
+ (DevicePolicyManager dpm, ComponentName admin, @UserIdInt int userId) ->
+ dpm.getMaximumTimeToLock(admin, userId) > 0);
}
private interface LockSettingCheck {
diff --git a/com/android/settingslib/Utils.java b/com/android/settingslib/Utils.java
index eb338427..3c46d999 100644
--- a/com/android/settingslib/Utils.java
+++ b/com/android/settingslib/Utils.java
@@ -14,8 +14,10 @@ import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
+import android.location.LocationManager;
import android.net.ConnectivityManager;
import android.os.BatteryManager;
+import android.os.UserHandle;
import android.os.UserManager;
import android.print.PrintManager;
import android.provider.Settings;
@@ -26,6 +28,10 @@ import com.android.settingslib.drawable.UserIconDrawable;
import java.text.NumberFormat;
public class Utils {
+
+ private static final String CURRENT_MODE_KEY = "CURRENT_MODE";
+ private static final String NEW_MODE_KEY = "NEW_MODE";
+
private static Signature[] sSystemSignature;
private static String sPermissionControllerPackageName;
private static String sServicesSystemSharedLibPackageName;
@@ -39,6 +45,16 @@ public class Utils {
com.android.internal.R.drawable.ic_wifi_signal_4
};
+ public static boolean updateLocationMode(Context context, int oldMode, int newMode, int userId) {
+ Intent intent = new Intent(LocationManager.MODE_CHANGING_ACTION);
+ intent.putExtra(CURRENT_MODE_KEY, oldMode);
+ intent.putExtra(NEW_MODE_KEY, newMode);
+ context.sendBroadcastAsUser(
+ intent, UserHandle.of(userId), android.Manifest.permission.WRITE_SECURE_SETTINGS);
+ return Settings.Secure.putIntForUser(
+ context.getContentResolver(), Settings.Secure.LOCATION_MODE, newMode, userId);
+ }
+
/**
* Return string resource that best describes combination of tethering
* options available on this device.
diff --git a/com/android/settingslib/applications/ApplicationsState.java b/com/android/settingslib/applications/ApplicationsState.java
index fa2499f6..5c73d548 100644
--- a/com/android/settingslib/applications/ApplicationsState.java
+++ b/com/android/settingslib/applications/ApplicationsState.java
@@ -21,6 +21,9 @@ import android.app.AppGlobals;
import android.app.Application;
import android.app.usage.StorageStats;
import android.app.usage.StorageStatsManager;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleObserver;
+import android.arch.lifecycle.OnLifecycleEvent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -52,11 +55,6 @@ import android.util.SparseArray;
import com.android.internal.R;
import com.android.internal.util.ArrayUtils;
-import com.android.settingslib.core.lifecycle.Lifecycle;
-import com.android.settingslib.core.lifecycle.LifecycleObserver;
-import com.android.settingslib.core.lifecycle.events.OnDestroy;
-import com.android.settingslib.core.lifecycle.events.OnPause;
-import com.android.settingslib.core.lifecycle.events.OnResume;
import java.io.File;
import java.io.IOException;
@@ -595,7 +593,7 @@ public class ApplicationsState {
.replaceAll("").toLowerCase();
}
- public class Session implements LifecycleObserver, OnPause, OnResume, OnDestroy {
+ public class Session implements LifecycleObserver {
final Callbacks mCallbacks;
boolean mResumed;
@@ -621,6 +619,7 @@ public class ApplicationsState {
}
}
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void onResume() {
if (DEBUG_LOCKING) Log.v(TAG, "resume about to acquire lock...");
synchronized (mEntriesMap) {
@@ -633,6 +632,7 @@ public class ApplicationsState {
if (DEBUG_LOCKING) Log.v(TAG, "...resume releasing lock");
}
+ @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void onPause() {
if (DEBUG_LOCKING) Log.v(TAG, "pause about to acquire lock...");
synchronized (mEntriesMap) {
@@ -752,6 +752,7 @@ public class ApplicationsState {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
+ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy() {
if (!mHasLifecycle) {
// TODO: Legacy, remove this later once all usages are switched to Lifecycle
diff --git a/com/android/settingslib/applications/ServiceListing.java b/com/android/settingslib/applications/ServiceListing.java
new file mode 100644
index 00000000..3c3c70ac
--- /dev/null
+++ b/com/android/settingslib/applications/ServiceListing.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 com.android.settingslib.applications;
+
+import android.app.ActivityManager;
+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.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.settingslib.wrapper.PackageManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Class for managing services matching a given intent and requesting a given permission.
+ */
+public class ServiceListing {
+ private final ContentResolver mContentResolver;
+ private final Context mContext;
+ private final String mTag;
+ private final String mSetting;
+ private final String mIntentAction;
+ private final String mPermission;
+ private final String mNoun;
+ private final HashSet<ComponentName> mEnabledServices = new HashSet<>();
+ private final List<ServiceInfo> mServices = new ArrayList<>();
+ private final List<Callback> mCallbacks = new ArrayList<>();
+
+ private boolean mListening;
+
+ private ServiceListing(Context context, String tag,
+ String setting, String intentAction, String permission, String noun) {
+ mContentResolver = context.getContentResolver();
+ mContext = context;
+ mTag = tag;
+ mSetting = setting;
+ mIntentAction = intentAction;
+ mPermission = permission;
+ mNoun = noun;
+ }
+
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
+ }
+
+ public void removeCallback(Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ public void setListening(boolean listening) {
+ if (mListening == listening) return;
+ mListening = listening;
+ if (mListening) {
+ // listen for package changes
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mPackageReceiver, filter);
+ mContentResolver.registerContentObserver(Settings.Secure.getUriFor(mSetting),
+ false, mSettingsObserver);
+ } else {
+ mContext.unregisterReceiver(mPackageReceiver);
+ mContentResolver.unregisterContentObserver(mSettingsObserver);
+ }
+ }
+
+ private void saveEnabledServices() {
+ StringBuilder sb = null;
+ for (ComponentName cn : mEnabledServices) {
+ if (sb == null) {
+ sb = new StringBuilder();
+ } else {
+ sb.append(':');
+ }
+ sb.append(cn.flattenToString());
+ }
+ Settings.Secure.putString(mContentResolver, mSetting,
+ sb != null ? sb.toString() : "");
+ }
+
+ private void loadEnabledServices() {
+ mEnabledServices.clear();
+ final String flat = Settings.Secure.getString(mContentResolver, mSetting);
+ if (flat != null && !"".equals(flat)) {
+ final String[] names = flat.split(":");
+ for (String name : names) {
+ final ComponentName cn = ComponentName.unflattenFromString(name);
+ if (cn != null) {
+ mEnabledServices.add(cn);
+ }
+ }
+ }
+ }
+
+ public void reload() {
+ loadEnabledServices();
+ mServices.clear();
+ final int user = ActivityManager.getCurrentUser();
+
+ final PackageManagerWrapper pmWrapper =
+ new PackageManagerWrapper(mContext.getPackageManager());
+ List<ResolveInfo> installedServices = pmWrapper.queryIntentServicesAsUser(
+ new Intent(mIntentAction),
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
+ user);
+
+ for (ResolveInfo resolveInfo : installedServices) {
+ ServiceInfo info = resolveInfo.serviceInfo;
+
+ if (!mPermission.equals(info.permission)) {
+ Slog.w(mTag, "Skipping " + mNoun + " service "
+ + info.packageName + "/" + info.name
+ + ": it does not require the permission "
+ + mPermission);
+ continue;
+ }
+ mServices.add(info);
+ }
+ for (Callback callback : mCallbacks) {
+ callback.onServicesReloaded(mServices);
+ }
+ }
+
+ public boolean isEnabled(ComponentName cn) {
+ return mEnabledServices.contains(cn);
+ }
+
+ public void setEnabled(ComponentName cn, boolean enabled) {
+ if (enabled) {
+ mEnabledServices.add(cn);
+ } else {
+ mEnabledServices.remove(cn);
+ }
+ saveEnabledServices();
+ }
+
+ private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ reload();
+ }
+ };
+
+ private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ reload();
+ }
+ };
+
+ public interface Callback {
+ void onServicesReloaded(List<ServiceInfo> services);
+ }
+
+ public static class Builder {
+ private final Context mContext;
+ private String mTag;
+ private String mSetting;
+ private String mIntentAction;
+ private String mPermission;
+ private String mNoun;
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ public Builder setTag(String tag) {
+ mTag = tag;
+ return this;
+ }
+
+ public Builder setSetting(String setting) {
+ mSetting = setting;
+ return this;
+ }
+
+ public Builder setIntentAction(String intentAction) {
+ mIntentAction = intentAction;
+ return this;
+ }
+
+ public Builder setPermission(String permission) {
+ mPermission = permission;
+ return this;
+ }
+
+ public Builder setNoun(String noun) {
+ mNoun = noun;
+ return this;
+ }
+
+ public ServiceListing build() {
+ return new ServiceListing(mContext, mTag, mSetting, mIntentAction, mPermission, mNoun);
+ }
+ }
+}
diff --git a/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 2873fb68..9caff100 100644
--- a/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -135,7 +135,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
if (profile instanceof MapProfile) {
profile.setPreferred(mDevice, true);
- } else if (!mProfiles.contains(profile)) {
+ }
+ if (!mProfiles.contains(profile)) {
mRemovedProfiles.remove(profile);
mProfiles.add(profile);
if (profile instanceof PanProfile &&
diff --git a/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 5b39ee4b..c3ff617b 100644
--- a/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -118,7 +118,7 @@ public class CachedBluetoothDeviceManager {
public synchronized void clearNonBondedDevices() {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
- if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
+ if (cachedDevice.getBondState() == BluetoothDevice.BOND_NONE) {
mCachedDevices.remove(i);
}
}
diff --git a/com/android/settingslib/bluetooth/LocalBluetoothManager.java b/com/android/settingslib/bluetooth/LocalBluetoothManager.java
index 1993b459..37324716 100644
--- a/com/android/settingslib/bluetooth/LocalBluetoothManager.java
+++ b/com/android/settingslib/bluetooth/LocalBluetoothManager.java
@@ -73,6 +73,7 @@ public class LocalBluetoothManager {
mCachedDeviceManager, context);
mProfileManager = new LocalBluetoothProfileManager(context,
mLocalAdapter, mCachedDeviceManager, mEventManager);
+ mEventManager.readPairedDevices();
}
public LocalBluetoothAdapter getBluetoothAdapter() {
diff --git a/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 9cda6693..991d9221 100644
--- a/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -25,6 +25,7 @@ import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothPan;
+import android.bluetooth.BluetoothPbap;
import android.bluetooth.BluetoothPbapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
@@ -140,9 +141,11 @@ public class LocalBluetoothProfileManager {
BluetoothMap.ACTION_CONNECTION_STATE_CHANGED);
}
- //Create PBAP server profile, but do not add it to list of profiles
- // as we do not need to monitor the profile as part of profile list
+ //Create PBAP server profile
+ if(DEBUG) Log.d(TAG, "Adding local PBAP profile");
mPbapProfile = new PbapServerProfile(context);
+ addProfile(mPbapProfile, PbapServerProfile.NAME,
+ BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED);
if (DEBUG) Log.d(TAG, "LocalBluetoothProfileManager construction complete");
}
@@ -495,6 +498,13 @@ public class LocalBluetoothProfileManager {
mMapProfile.setPreferred(device, true);
}
+ if ((mPbapProfile != null) &&
+ (mPbapProfile.getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED)) {
+ profiles.add(mPbapProfile);
+ removedProfiles.remove(mPbapProfile);
+ mPbapProfile.setPreferred(device, true);
+ }
+
if (mMapClientProfile != null) {
profiles.add(mMapClientProfile);
removedProfiles.remove(mMapClientProfile);
@@ -503,8 +513,6 @@ public class LocalBluetoothProfileManager {
if (mUsePbapPce) {
profiles.add(mPbapClientProfile);
removedProfiles.remove(mPbapClientProfile);
- profiles.remove(mPbapProfile);
- removedProfiles.add(mPbapProfile);
}
if (DEBUG) {
diff --git a/com/android/settingslib/bluetooth/PbapServerProfile.java b/com/android/settingslib/bluetooth/PbapServerProfile.java
index f3b69125..58465f29 100644
--- a/com/android/settingslib/bluetooth/PbapServerProfile.java
+++ b/com/android/settingslib/bluetooth/PbapServerProfile.java
@@ -91,7 +91,7 @@ public class PbapServerProfile implements LocalBluetoothProfile {
public boolean disconnect(BluetoothDevice device) {
if (mService == null) return false;
- return mService.disconnect();
+ return mService.disconnect(device);
}
public int getConnectionStatus(BluetoothDevice device) {
diff --git a/com/android/settingslib/datetime/ZoneGetter.java b/com/android/settingslib/datetime/ZoneGetter.java
index a8262c8c..974b2a43 100644
--- a/com/android/settingslib/datetime/ZoneGetter.java
+++ b/com/android/settingslib/datetime/ZoneGetter.java
@@ -271,7 +271,7 @@ public class ZoneGetter {
* @param now The current time, used to tell whether daylight savings is active.
* @return A CharSequence suitable for display as the offset label of {@code tz}.
*/
- private static CharSequence getGmtOffsetText(TimeZoneFormat tzFormatter, Locale locale,
+ public static CharSequence getGmtOffsetText(TimeZoneFormat tzFormatter, Locale locale,
TimeZone tz, Date now) {
final SpannableStringBuilder builder = new SpannableStringBuilder();
diff --git a/com/android/settingslib/drawer/DashboardCategory.java b/com/android/settingslib/drawer/DashboardCategory.java
index a966e824..3a03644b 100644
--- a/com/android/settingslib/drawer/DashboardCategory.java
+++ b/com/android/settingslib/drawer/DashboardCategory.java
@@ -75,23 +75,27 @@ public class DashboardCategory implements Parcelable {
* Note: the returned list serves as a read-only list. If tiles needs to be added or removed
* from the actual tiles list, it should be done through {@link #addTile}, {@link #removeTile}.
*/
- public List<Tile> getTiles() {
- return Collections.unmodifiableList(mTiles);
+ public synchronized List<Tile> getTiles() {
+ final List<Tile> result = new ArrayList<>(mTiles.size());
+ for (Tile tile : mTiles) {
+ result.add(tile);
+ }
+ return result;
}
- public void addTile(Tile tile) {
+ public synchronized void addTile(Tile tile) {
mTiles.add(tile);
}
- public void addTile(int n, Tile tile) {
+ public synchronized void addTile(int n, Tile tile) {
mTiles.add(n, tile);
}
- public void removeTile(Tile tile) {
+ public synchronized void removeTile(Tile tile) {
mTiles.remove(tile);
}
- public void removeTile(int n) {
+ public synchronized void removeTile(int n) {
mTiles.remove(n);
}
@@ -103,7 +107,7 @@ public class DashboardCategory implements Parcelable {
return mTiles.get(n);
}
- public boolean containsComponent(ComponentName component) {
+ public synchronized boolean containsComponent(ComponentName component) {
for (Tile tile : mTiles) {
if (TextUtils.equals(tile.intent.getComponent().getClassName(),
component.getClassName())) {
@@ -129,7 +133,7 @@ public class DashboardCategory implements Parcelable {
/**
* Sort priority value and package name for tiles in this category.
*/
- public void sortTiles(String skipPackageName) {
+ public synchronized void sortTiles(String skipPackageName) {
// Sort mTiles based on [priority, package within priority]
Collections.sort(mTiles, (tile1, tile2) -> {
final String package1 = tile1.intent.getComponent().getPackageName();
diff --git a/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java b/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
new file mode 100644
index 00000000..2b6d09f9
--- /dev/null
+++ b/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
@@ -0,0 +1,108 @@
+/*
+ * 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 com.android.settingslib.fuelgauge;
+
+import android.os.IDeviceIdleController;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
+import android.util.Log;
+
+/**
+ * Handles getting/changing the whitelist for the exceptions to battery saving features.
+ */
+public class PowerWhitelistBackend {
+
+ private static final String TAG = "PowerWhitelistBackend";
+
+ private static final String DEVICE_IDLE_SERVICE = "deviceidle";
+
+ private static PowerWhitelistBackend sInstance;
+
+ private final IDeviceIdleController mDeviceIdleService;
+ private final ArraySet<String> mWhitelistedApps = new ArraySet<>();
+ private final ArraySet<String> mSysWhitelistedApps = new ArraySet<>();
+
+ public PowerWhitelistBackend() {
+ mDeviceIdleService = IDeviceIdleController.Stub.asInterface(
+ ServiceManager.getService(DEVICE_IDLE_SERVICE));
+ refreshList();
+ }
+
+ @VisibleForTesting
+ PowerWhitelistBackend(IDeviceIdleController deviceIdleService) {
+ mDeviceIdleService = deviceIdleService;
+ refreshList();
+ }
+
+ public int getWhitelistSize() {
+ return mWhitelistedApps.size();
+ }
+
+ public boolean isSysWhitelisted(String pkg) {
+ return mSysWhitelistedApps.contains(pkg);
+ }
+
+ public boolean isWhitelisted(String pkg) {
+ return mWhitelistedApps.contains(pkg);
+ }
+
+ public void addApp(String pkg) {
+ try {
+ mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
+ mWhitelistedApps.add(pkg);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+ }
+ }
+
+ public void removeApp(String pkg) {
+ try {
+ mDeviceIdleService.removePowerSaveWhitelistApp(pkg);
+ mWhitelistedApps.remove(pkg);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+ }
+ }
+
+ @VisibleForTesting
+ public void refreshList() {
+ mSysWhitelistedApps.clear();
+ mWhitelistedApps.clear();
+ try {
+ String[] whitelistedApps = mDeviceIdleService.getFullPowerWhitelist();
+ for (String app : whitelistedApps) {
+ mWhitelistedApps.add(app);
+ }
+ String[] sysWhitelistedApps = mDeviceIdleService.getSystemPowerWhitelist();
+ for (String app : sysWhitelistedApps) {
+ mSysWhitelistedApps.add(app);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+ }
+ }
+
+ public static PowerWhitelistBackend getInstance() {
+ if (sInstance == null) {
+ sInstance = new PowerWhitelistBackend();
+ }
+ return sInstance;
+ }
+
+}
diff --git a/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java b/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java
new file mode 100644
index 00000000..e3e27ce1
--- /dev/null
+++ b/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java
@@ -0,0 +1,292 @@
+/*
+ * 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 com.android.settingslib.license;
+
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * The utility class that generate a license html file from xml files.
+ * All the HTML snippets and logic are copied from build/make/tools/generate-notice-files.py.
+ *
+ * TODO: Remove duplicate codes once backward support ends.
+ */
+class LicenseHtmlGeneratorFromXml {
+ private static final String TAG = "LicenseHtmlGeneratorFromXml";
+
+ private static final String TAG_ROOT = "licenses";
+ private static final String TAG_FILE_NAME = "file-name";
+ private static final String TAG_FILE_CONTENT = "file-content";
+ private static final String ATTR_CONTENT_ID = "contentId";
+
+ private static final String HTML_HEAD_STRING =
+ "<html><head>\n"
+ + "<style type=\"text/css\">\n"
+ + "body { padding: 0; font-family: sans-serif; }\n"
+ + ".same-license { background-color: #eeeeee;\n"
+ + " border-top: 20px solid white;\n"
+ + " padding: 10px; }\n"
+ + ".label { font-weight: bold; }\n"
+ + ".file-list { margin-left: 1em; color: blue; }\n"
+ + "</style>\n"
+ + "</head>"
+ + "<body topmargin=\"0\" leftmargin=\"0\" rightmargin=\"0\" bottommargin=\"0\">\n"
+ + "<div class=\"toc\">\n"
+ + "<ul>";
+
+ private static final String HTML_MIDDLE_STRING =
+ "</ul>\n"
+ + "</div><!-- table of contents -->\n"
+ + "<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">";
+
+ private static final String HTML_REAR_STRING =
+ "</table></body></html>";
+
+ private final List<File> mXmlFiles;
+
+ /*
+ * A map from a file name to a content id (MD5 sum of file content) for its license.
+ * For example, "/system/priv-app/TeleService/TeleService.apk" maps to
+ * "9645f39e9db895a4aa6e02cb57294595". Here "9645f39e9db895a4aa6e02cb57294595" is a MD5 sum
+ * of the content of packages/services/Telephony/MODULE_LICENSE_APACHE2.
+ */
+ private final Map<String, String> mFileNameToContentIdMap = new HashMap();
+
+ /*
+ * A map from a content id (MD5 sum of file content) to a license file content.
+ * For example, "9645f39e9db895a4aa6e02cb57294595" maps to the content string of
+ * packages/services/Telephony/MODULE_LICENSE_APACHE2. Here "9645f39e9db895a4aa6e02cb57294595"
+ * is a MD5 sum of the file content.
+ */
+ private final Map<String, String> mContentIdToFileContentMap = new HashMap();
+
+ static class ContentIdAndFileNames {
+ final String mContentId;
+ final List<String> mFileNameList = new ArrayList();
+
+ ContentIdAndFileNames(String contentId) {
+ mContentId = contentId;
+ }
+ }
+
+ private LicenseHtmlGeneratorFromXml(List<File> xmlFiles) {
+ mXmlFiles = xmlFiles;
+ }
+
+ public static boolean generateHtml(List<File> xmlFiles, File outputFile) {
+ LicenseHtmlGeneratorFromXml genertor = new LicenseHtmlGeneratorFromXml(xmlFiles);
+ return genertor.generateHtml(outputFile);
+ }
+
+ private boolean generateHtml(File outputFile) {
+ for (File xmlFile : mXmlFiles) {
+ parse(xmlFile);
+ }
+
+ if (mFileNameToContentIdMap.isEmpty() || mContentIdToFileContentMap.isEmpty()) {
+ return false;
+ }
+
+ PrintWriter writer = null;
+ try {
+ writer = new PrintWriter(outputFile);
+
+ generateHtml(mFileNameToContentIdMap, mContentIdToFileContentMap, writer);
+
+ writer.flush();
+ writer.close();
+ return true;
+ } catch (FileNotFoundException | SecurityException e) {
+ Log.e(TAG, "Failed to generate " + outputFile, e);
+
+ if (writer != null) {
+ writer.close();
+ }
+ return false;
+ }
+ }
+
+ private void parse(File xmlFile) {
+ if (xmlFile == null || !xmlFile.exists() || xmlFile.length() == 0) {
+ return;
+ }
+
+ InputStreamReader in = null;
+ try {
+ if (xmlFile.getName().endsWith(".gz")) {
+ in = new InputStreamReader(new GZIPInputStream(new FileInputStream(xmlFile)));
+ } else {
+ in = new FileReader(xmlFile);
+ }
+
+ parse(in, mFileNameToContentIdMap, mContentIdToFileContentMap);
+
+ in.close();
+ } catch (XmlPullParserException | IOException e) {
+ Log.e(TAG, "Failed to parse " + xmlFile, e);
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ie) {
+ Log.w(TAG, "Failed to close " + xmlFile);
+ }
+ }
+ }
+ }
+
+ /*
+ * Parses an input stream and fills a map from a file name to a content id for its license
+ * and a map from a content id to a license file content.
+ *
+ * Following xml format is expected from the input stream.
+ *
+ * <licenses>
+ * <file-name contentId="content_id_of_license1">file1</file-name>
+ * <file-name contentId="content_id_of_license2">file2</file-name>
+ * ...
+ * <file-content contentId="content_id_of_license1">license1 file contents</file-content>
+ * <file-content contentId="content_id_of_license2">license2 file contents</file-content>
+ * ...
+ * </licenses>
+ */
+ @VisibleForTesting
+ static void parse(InputStreamReader in, Map<String, String> outFileNameToContentIdMap,
+ Map<String, String> outContentIdToFileContentMap)
+ throws XmlPullParserException, IOException {
+ Map<String, String> fileNameToContentIdMap = new HashMap<String, String>();
+ Map<String, String> contentIdToFileContentMap = new HashMap<String, String>();
+
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(in);
+ parser.nextTag();
+
+ parser.require(XmlPullParser.START_TAG, "", TAG_ROOT);
+
+ int state = parser.getEventType();
+ while (state != XmlPullParser.END_DOCUMENT) {
+ if (state == XmlPullParser.START_TAG) {
+ if (TAG_FILE_NAME.equals(parser.getName())) {
+ String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID);
+ if (!TextUtils.isEmpty(contentId)) {
+ String fileName = readText(parser).trim();
+ if (!TextUtils.isEmpty(fileName)) {
+ fileNameToContentIdMap.put(fileName, contentId);
+ }
+ }
+ } else if (TAG_FILE_CONTENT.equals(parser.getName())) {
+ String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID);
+ if (!TextUtils.isEmpty(contentId)
+ && !outContentIdToFileContentMap.containsKey(contentId)
+ && !contentIdToFileContentMap.containsKey(contentId)) {
+ String fileContent = readText(parser);
+ if (!TextUtils.isEmpty(fileContent)) {
+ contentIdToFileContentMap.put(contentId, fileContent);
+ }
+ }
+ }
+ }
+
+ state = parser.next();
+ }
+ outFileNameToContentIdMap.putAll(fileNameToContentIdMap);
+ outContentIdToFileContentMap.putAll(contentIdToFileContentMap);
+ }
+
+ private static String readText(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ StringBuffer result = new StringBuffer();
+ int state = parser.next();
+ while (state == XmlPullParser.TEXT) {
+ result.append(parser.getText());
+ state = parser.next();
+ }
+ return result.toString();
+ }
+
+ @VisibleForTesting
+ static void generateHtml(Map<String, String> fileNameToContentIdMap,
+ Map<String, String> contentIdToFileContentMap, PrintWriter writer) {
+ List<String> fileNameList = new ArrayList();
+ fileNameList.addAll(fileNameToContentIdMap.keySet());
+ Collections.sort(fileNameList);
+
+ writer.println(HTML_HEAD_STRING);
+
+ int count = 0;
+ Map<String, Integer> contentIdToOrderMap = new HashMap();
+ List<ContentIdAndFileNames> contentIdAndFileNamesList = new ArrayList();
+
+ // Prints all the file list with a link to its license file content.
+ for (String fileName : fileNameList) {
+ String contentId = fileNameToContentIdMap.get(fileName);
+ // Assigns an id to a newly referred license file content.
+ if (!contentIdToOrderMap.containsKey(contentId)) {
+ contentIdToOrderMap.put(contentId, count);
+
+ // An index in contentIdAndFileNamesList is the order of each element.
+ contentIdAndFileNamesList.add(new ContentIdAndFileNames(contentId));
+ count++;
+ }
+
+ int id = contentIdToOrderMap.get(contentId);
+ contentIdAndFileNamesList.get(id).mFileNameList.add(fileName);
+ writer.format("<li><a href=\"#id%d\">%s</a></li>\n", id, fileName);
+ }
+
+ writer.println(HTML_MIDDLE_STRING);
+
+ count = 0;
+ // Prints all contents of the license files in order of id.
+ for (ContentIdAndFileNames contentIdAndFileNames : contentIdAndFileNamesList) {
+ writer.format("<tr id=\"id%d\"><td class=\"same-license\">\n", count);
+ writer.println("<div class=\"label\">Notices for file(s):</div>");
+ writer.println("<div class=\"file-list\">");
+ for (String fileName : contentIdAndFileNames.mFileNameList) {
+ writer.format("%s <br/>\n", fileName);
+ }
+ writer.println("</div><!-- file-list -->");
+ writer.println("<pre class=\"license-text\">");
+ writer.println(contentIdToFileContentMap.get(
+ contentIdAndFileNames.mContentId));
+ writer.println("</pre><!-- license-text -->");
+ writer.println("</td></tr><!-- same-license -->");
+
+ count++;
+ }
+
+ writer.println(HTML_REAR_STRING);
+ }
+}
diff --git a/com/android/settingslib/license/LicenseHtmlLoader.java b/com/android/settingslib/license/LicenseHtmlLoader.java
new file mode 100644
index 00000000..a9fb20ca
--- /dev/null
+++ b/com/android/settingslib/license/LicenseHtmlLoader.java
@@ -0,0 +1,110 @@
+/*
+ * 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 com.android.settingslib.license;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.settingslib.utils.AsyncLoader;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * LicenseHtmlLoader is a loader which loads a license html file from default license xml files.
+ */
+public class LicenseHtmlLoader extends AsyncLoader<File> {
+ private static final String TAG = "LicenseHtmlLoader";
+
+ private static final String[] DEFAULT_LICENSE_XML_PATHS = {
+ "/system/etc/NOTICE.xml.gz",
+ "/vendor/etc/NOTICE.xml.gz",
+ "/odm/etc/NOTICE.xml.gz",
+ "/oem/etc/NOTICE.xml.gz"};
+ private static final String NOTICE_HTML_FILE_NAME = "NOTICE.html";
+
+ private Context mContext;
+
+ public LicenseHtmlLoader(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ public File loadInBackground() {
+ return generateHtmlFromDefaultXmlFiles();
+ }
+
+ @Override
+ protected void onDiscardResult(File f) {
+ }
+
+ private File generateHtmlFromDefaultXmlFiles() {
+ final List<File> xmlFiles = getVaildXmlFiles();
+ if (xmlFiles.isEmpty()) {
+ Log.e(TAG, "No notice file exists.");
+ return null;
+ }
+
+ File cachedHtmlFile = getCachedHtmlFile();
+ if (!isCachedHtmlFileOutdated(xmlFiles, cachedHtmlFile)
+ || generateHtmlFile(xmlFiles, cachedHtmlFile)) {
+ return cachedHtmlFile;
+ }
+
+ return null;
+ }
+
+ @VisibleForTesting
+ List<File> getVaildXmlFiles() {
+ final List<File> xmlFiles = new ArrayList();
+ for (final String xmlPath : DEFAULT_LICENSE_XML_PATHS) {
+ File file = new File(xmlPath);
+ if (file.exists() && file.length() != 0) {
+ xmlFiles.add(file);
+ }
+ }
+ return xmlFiles;
+ }
+
+ @VisibleForTesting
+ File getCachedHtmlFile() {
+ return new File(mContext.getCacheDir(), NOTICE_HTML_FILE_NAME);
+ }
+
+ @VisibleForTesting
+ boolean isCachedHtmlFileOutdated(List<File> xmlFiles, File cachedHtmlFile) {
+ boolean outdated = true;
+ if (cachedHtmlFile.exists() && cachedHtmlFile.length() != 0) {
+ outdated = false;
+ for (File file : xmlFiles) {
+ if (cachedHtmlFile.lastModified() < file.lastModified()) {
+ outdated = true;
+ break;
+ }
+ }
+ }
+ return outdated;
+ }
+
+ @VisibleForTesting
+ boolean generateHtmlFile(List<File> xmlFiles, File htmlFile) {
+ return LicenseHtmlGeneratorFromXml.generateHtml(xmlFiles, htmlFile);
+ }
+}
diff --git a/com/android/settingslib/location/RecentLocationApps.java b/com/android/settingslib/location/RecentLocationApps.java
index 3f826cc0..6025d68a 100644
--- a/com/android/settingslib/location/RecentLocationApps.java
+++ b/com/android/settingslib/location/RecentLocationApps.java
@@ -16,21 +16,21 @@
package com.android.settingslib.location;
-import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.os.Process;
-import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.support.annotation.VisibleForTesting;
import android.util.IconDrawableFactory;
import android.util.Log;
-
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
/**
@@ -38,11 +38,13 @@ import java.util.List;
*/
public class RecentLocationApps {
private static final String TAG = RecentLocationApps.class.getSimpleName();
- private static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
+ @VisibleForTesting
+ static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
private static final int RECENT_TIME_INTERVAL_MILLIS = 15 * 60 * 1000;
- private static final int[] LOCATION_OPS = new int[] {
+ @VisibleForTesting
+ static final int[] LOCATION_OPS = new int[] {
AppOpsManager.OP_MONITOR_LOCATION,
AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION,
};
@@ -59,6 +61,7 @@ public class RecentLocationApps {
/**
* Fills a list of applications which queried location recently within specified time.
+ * Apps are sorted by recency. Apps with more recent location requests are in the front.
*/
public List<Request> getAppList() {
// Retrieve a location usage list from AppOps
@@ -91,7 +94,18 @@ public class RecentLocationApps {
requests.add(request);
}
}
+ return requests;
+ }
+ public List<Request> getAppListSorted() {
+ List<Request> requests = getAppList();
+ // Sort the list of Requests by recency. Most recent request first.
+ Collections.sort(requests, Collections.reverseOrder(new Comparator<Request>() {
+ @Override
+ public int compare(Request request1, Request request2) {
+ return Long.compare(request1.requestFinishTime, request2.requestFinishTime);
+ }
+ }));
return requests;
}
@@ -108,10 +122,12 @@ public class RecentLocationApps {
List<AppOpsManager.OpEntry> entries = ops.getOps();
boolean highBattery = false;
boolean normalBattery = false;
+ long locationRequestFinishTime = 0L;
// Earliest time for a location request to end and still be shown in list.
long recentLocationCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
for (AppOpsManager.OpEntry entry : entries) {
if (entry.isRunning() || entry.getTime() >= recentLocationCutoffTime) {
+ locationRequestFinishTime = entry.getTime() + entry.getDuration();
switch (entry.getOp()) {
case AppOpsManager.OP_MONITOR_LOCATION:
normalBattery = true;
@@ -133,15 +149,13 @@ public class RecentLocationApps {
}
// The package is fresh enough, continue.
-
int uid = ops.getUid();
int userId = UserHandle.getUserId(uid);
Request request = null;
try {
- IPackageManager ipm = AppGlobals.getPackageManager();
- ApplicationInfo appInfo =
- ipm.getApplicationInfo(packageName, PackageManager.GET_META_DATA, userId);
+ ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
+ packageName, PackageManager.GET_META_DATA, userId);
if (appInfo == null) {
Log.w(TAG, "Null application info retrieved for package " + packageName
+ ", userId " + userId);
@@ -158,12 +172,10 @@ public class RecentLocationApps {
badgedAppLabel = null;
}
request = new Request(packageName, userHandle, icon, appLabel, highBattery,
- badgedAppLabel);
- } catch (RemoteException e) {
- Log.w(TAG, "Error while retrieving application info for package " + packageName
- + ", userId " + userId, e);
+ badgedAppLabel, locationRequestFinishTime);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "package name not found for " + packageName + ", userId " + userId);
}
-
return request;
}
@@ -174,15 +186,18 @@ public class RecentLocationApps {
public final CharSequence label;
public final boolean isHighBattery;
public final CharSequence contentDescription;
+ public final long requestFinishTime;
private Request(String packageName, UserHandle userHandle, Drawable icon,
- CharSequence label, boolean isHighBattery, CharSequence contentDescription) {
+ CharSequence label, boolean isHighBattery, CharSequence contentDescription,
+ long requestFinishTime) {
this.packageName = packageName;
this.userHandle = userHandle;
this.icon = icon;
this.label = label;
this.isHighBattery = isHighBattery;
this.contentDescription = contentDescription;
+ this.requestFinishTime = requestFinishTime;
}
}
}
diff --git a/com/android/settingslib/utils/AsyncLoader.java b/com/android/settingslib/utils/AsyncLoader.java
new file mode 100644
index 00000000..06770ac0
--- /dev/null
+++ b/com/android/settingslib/utils/AsyncLoader.java
@@ -0,0 +1,110 @@
+/*
+ * 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 com.android.settingslib.utils;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+
+/**
+ * This class fills in some boilerplate for AsyncTaskLoader to actually load things.
+ *
+ * Subclasses need to implement {@link AsyncLoader#loadInBackground()} to perform the actual
+ * background task, and {@link AsyncLoader#onDiscardResult(T)} to clean up previously loaded
+ * results.
+ *
+ * This loader is based on the MailAsyncTaskLoader from the AOSP EmailUnified repo.
+ *
+ * @param <T> the data type to be loaded.
+ */
+public abstract class AsyncLoader<T> extends AsyncTaskLoader<T> {
+ private T mResult;
+
+ public AsyncLoader(final Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mResult != null) {
+ deliverResult(mResult);
+ }
+
+ if (takeContentChanged() || mResult == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ public void deliverResult(final T data) {
+ if (isReset()) {
+ if (data != null) {
+ onDiscardResult(data);
+ }
+ return;
+ }
+
+ final T oldResult = mResult;
+ mResult = data;
+
+ if (isStarted()) {
+ super.deliverResult(data);
+ }
+
+ if (oldResult != null && oldResult != mResult) {
+ onDiscardResult(oldResult);
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ onStopLoading();
+
+ if (mResult != null) {
+ onDiscardResult(mResult);
+ }
+ mResult = null;
+ }
+
+ @Override
+ public void onCanceled(final T data) {
+ super.onCanceled(data);
+
+ if (data != null) {
+ onDiscardResult(data);
+ }
+ }
+
+ /**
+ * Called when discarding the load results so subclasses can take care of clean-up or
+ * recycling tasks. This is not called if the same result (by way of pointer equality) is
+ * returned again by a subsequent call to loadInBackground, or if result is null.
+ *
+ * Note that this may be called concurrently with loadInBackground(), and in some circumstances
+ * may be called more than once for a given object.
+ *
+ * @param result The value returned from {@link AsyncLoader#loadInBackground()} which
+ * is to be discarded.
+ */
+ protected abstract void onDiscardResult(T result);
+}
diff --git a/com/android/settingslib/wifi/AccessPoint.java b/com/android/settingslib/wifi/AccessPoint.java
index 58f12261..754b8811 100644
--- a/com/android/settingslib/wifi/AccessPoint.java
+++ b/com/android/settingslib/wifi/AccessPoint.java
@@ -622,6 +622,14 @@ public class AccessPoint implements Comparable<AccessPoint> {
return mRssi;
}
+ public ConcurrentHashMap<String, ScanResult> getScanResults() {
+ return mScanResultCache;
+ }
+
+ public Map<String, TimestampedScoredNetwork> getScoredNetworkCache() {
+ return mScoredNetworkCache;
+ }
+
/**
* Updates {@link #mRssi}.
*
@@ -845,41 +853,8 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
if (WifiTracker.sVerboseLogging) {
- // Add RSSI/band information for this config, what was seen up to 6 seconds ago
- // verbose WiFi Logging is only turned on thru developers settings
- if (isActive() && mInfo != null) {
- summary.append(" f=" + Integer.toString(mInfo.getFrequency()));
- }
- summary.append(" " + getVisibilityStatus());
- if (config != null && !config.getNetworkSelectionStatus().isNetworkEnabled()) {
- summary.append(" (" + config.getNetworkSelectionStatus().getNetworkStatusString());
- if (config.getNetworkSelectionStatus().getDisableTime() > 0) {
- long now = System.currentTimeMillis();
- long diff = (now - config.getNetworkSelectionStatus().getDisableTime()) / 1000;
- long sec = diff%60; //seconds
- long min = (diff/60)%60; //minutes
- long hour = (min/60)%60; //hours
- summary.append(", ");
- if (hour > 0) summary.append(Long.toString(hour) + "h ");
- summary.append( Long.toString(min) + "m ");
- summary.append( Long.toString(sec) + "s ");
- }
- summary.append(")");
- }
-
- if (config != null) {
- WifiConfiguration.NetworkSelectionStatus networkStatus =
- config.getNetworkSelectionStatus();
- for (int index = WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE;
- index < WifiConfiguration.NetworkSelectionStatus
- .NETWORK_SELECTION_DISABLED_MAX; index++) {
- if (networkStatus.getDisableReasonCounter(index) != 0) {
- summary.append(" " + WifiConfiguration.NetworkSelectionStatus
- .getNetworkDisableReasonString(index) + "="
- + networkStatus.getDisableReasonCounter(index));
- }
- }
- }
+ evictOldScanResults();
+ summary.append(WifiUtils.buildLoggingSummary(this, config));
}
// If Speed label and summary are both present, use the preference combination to combine
@@ -897,127 +872,6 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
/**
- * Returns the visibility status of the WifiConfiguration.
- *
- * @return autojoin debugging information
- * TODO: use a string formatter
- * ["rssi 5Ghz", "num results on 5GHz" / "rssi 5Ghz", "num results on 5GHz"]
- * For instance [-40,5/-30,2]
- */
- private String getVisibilityStatus() {
- StringBuilder visibility = new StringBuilder();
- StringBuilder scans24GHz = new StringBuilder();
- StringBuilder scans5GHz = new StringBuilder();
- String bssid = null;
-
- long now = System.currentTimeMillis();
-
- if (isActive() && mInfo != null) {
- bssid = mInfo.getBSSID();
- if (bssid != null) {
- visibility.append(" ").append(bssid);
- }
- visibility.append(" rssi=").append(mInfo.getRssi());
- visibility.append(" ");
- visibility.append(" score=").append(mInfo.score);
- if (mSpeed != Speed.NONE) {
- visibility.append(" speed=").append(getSpeedLabel());
- }
- visibility.append(String.format(" tx=%.1f,", mInfo.txSuccessRate));
- visibility.append(String.format("%.1f,", mInfo.txRetriesRate));
- visibility.append(String.format("%.1f ", mInfo.txBadRate));
- visibility.append(String.format("rx=%.1f", mInfo.rxSuccessRate));
- }
-
- int maxRssi5 = WifiConfiguration.INVALID_RSSI;
- int maxRssi24 = WifiConfiguration.INVALID_RSSI;
- final int maxDisplayedScans = 4;
- int num5 = 0; // number of scanned BSSID on 5GHz band
- int num24 = 0; // number of scanned BSSID on 2.4Ghz band
- int numBlackListed = 0;
- evictOldScanResults();
-
- // TODO: sort list by RSSI or age
- long nowMs = SystemClock.elapsedRealtime();
- for (ScanResult result : mScanResultCache.values()) {
- if (result.frequency >= LOWER_FREQ_5GHZ
- && result.frequency <= HIGHER_FREQ_5GHZ) {
- // Strictly speaking: [4915, 5825]
- num5++;
-
- if (result.level > maxRssi5) {
- maxRssi5 = result.level;
- }
- if (num5 <= maxDisplayedScans) {
- scans5GHz.append(verboseScanResultSummary(result, bssid, nowMs));
- }
- } else if (result.frequency >= LOWER_FREQ_24GHZ
- && result.frequency <= HIGHER_FREQ_24GHZ) {
- // Strictly speaking: [2412, 2482]
- num24++;
-
- if (result.level > maxRssi24) {
- maxRssi24 = result.level;
- }
- if (num24 <= maxDisplayedScans) {
- scans24GHz.append(verboseScanResultSummary(result, bssid, nowMs));
- }
- }
- }
- visibility.append(" [");
- if (num24 > 0) {
- visibility.append("(").append(num24).append(")");
- if (num24 > maxDisplayedScans) {
- visibility.append("max=").append(maxRssi24).append(",");
- }
- visibility.append(scans24GHz.toString());
- }
- visibility.append(";");
- if (num5 > 0) {
- visibility.append("(").append(num5).append(")");
- if (num5 > maxDisplayedScans) {
- visibility.append("max=").append(maxRssi5).append(",");
- }
- visibility.append(scans5GHz.toString());
- }
- if (numBlackListed > 0)
- visibility.append("!").append(numBlackListed);
- visibility.append("]");
-
- return visibility.toString();
- }
-
- @VisibleForTesting
- /* package */ String verboseScanResultSummary(ScanResult result, String bssid, long nowMs) {
- StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.append(" \n{").append(result.BSSID);
- if (result.BSSID.equals(bssid)) {
- stringBuilder.append("*");
- }
- stringBuilder.append("=").append(result.frequency);
- stringBuilder.append(",").append(result.level);
- int speed = getSpecificApSpeed(result);
- if (speed != Speed.NONE) {
- stringBuilder.append(",")
- .append(getSpeedLabel(speed));
- }
- int ageSeconds = (int) (nowMs - result.timestamp / 1000) / 1000;
- stringBuilder.append(",").append(ageSeconds).append("s");
- stringBuilder.append("}");
- return stringBuilder.toString();
- }
-
- @Speed private int getSpecificApSpeed(ScanResult result) {
- TimestampedScoredNetwork timedScore = mScoredNetworkCache.get(result.BSSID);
- if (timedScore == null) {
- return Speed.NONE;
- }
- // For debugging purposes we may want to use mRssi rather than result.level as the average
- // speed wil be determined by mRssi
- return timedScore.getScore().calculateBadge(result.level);
- }
-
- /**
* Return whether this is the active connection.
* For ephemeral connections (networkId is invalid), this returns false if the network is
* disconnected.
@@ -1275,7 +1129,7 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
@Nullable
- private String getSpeedLabel(@Speed int speed) {
+ String getSpeedLabel(@Speed int speed) {
switch (speed) {
case Speed.VERY_FAST:
return mContext.getString(R.string.speed_label_very_fast);
diff --git a/com/android/settingslib/wifi/WifiUtils.java b/com/android/settingslib/wifi/WifiUtils.java
new file mode 100644
index 00000000..932c6fd8
--- /dev/null
+++ b/com/android/settingslib/wifi/WifiUtils.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 com.android.settingslib.wifi;
+
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.os.SystemClock;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.Map;
+
+public class WifiUtils {
+
+ public static String buildLoggingSummary(AccessPoint accessPoint, WifiConfiguration config) {
+ final StringBuilder summary = new StringBuilder();
+ final WifiInfo info = accessPoint.getInfo();
+ // Add RSSI/band information for this config, what was seen up to 6 seconds ago
+ // verbose WiFi Logging is only turned on thru developers settings
+ if (accessPoint.isActive() && info != null) {
+ summary.append(" f=" + Integer.toString(info.getFrequency()));
+ }
+ summary.append(" " + getVisibilityStatus(accessPoint));
+ if (config != null && !config.getNetworkSelectionStatus().isNetworkEnabled()) {
+ summary.append(" (" + config.getNetworkSelectionStatus().getNetworkStatusString());
+ if (config.getNetworkSelectionStatus().getDisableTime() > 0) {
+ long now = System.currentTimeMillis();
+ long diff = (now - config.getNetworkSelectionStatus().getDisableTime()) / 1000;
+ long sec = diff % 60; //seconds
+ long min = (diff / 60) % 60; //minutes
+ long hour = (min / 60) % 60; //hours
+ summary.append(", ");
+ if (hour > 0) summary.append(Long.toString(hour) + "h ");
+ summary.append(Long.toString(min) + "m ");
+ summary.append(Long.toString(sec) + "s ");
+ }
+ summary.append(")");
+ }
+
+ if (config != null) {
+ WifiConfiguration.NetworkSelectionStatus networkStatus =
+ config.getNetworkSelectionStatus();
+ for (int index = WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE;
+ index < WifiConfiguration.NetworkSelectionStatus
+ .NETWORK_SELECTION_DISABLED_MAX; index++) {
+ if (networkStatus.getDisableReasonCounter(index) != 0) {
+ summary.append(" " + WifiConfiguration.NetworkSelectionStatus
+ .getNetworkDisableReasonString(index) + "="
+ + networkStatus.getDisableReasonCounter(index));
+ }
+ }
+ }
+
+ return summary.toString();
+ }
+
+ /**
+ * Returns the visibility status of the WifiConfiguration.
+ *
+ * @return autojoin debugging information
+ * TODO: use a string formatter
+ * ["rssi 5Ghz", "num results on 5GHz" / "rssi 5Ghz", "num results on 5GHz"]
+ * For instance [-40,5/-30,2]
+ */
+ private static String getVisibilityStatus(AccessPoint accessPoint) {
+ final WifiInfo info = accessPoint.getInfo();
+ StringBuilder visibility = new StringBuilder();
+ StringBuilder scans24GHz = new StringBuilder();
+ StringBuilder scans5GHz = new StringBuilder();
+ String bssid = null;
+
+ if (accessPoint.isActive() && info != null) {
+ bssid = info.getBSSID();
+ if (bssid != null) {
+ visibility.append(" ").append(bssid);
+ }
+ visibility.append(" rssi=").append(info.getRssi());
+ visibility.append(" ");
+ visibility.append(" score=").append(info.score);
+ if (accessPoint.getSpeed() != AccessPoint.Speed.NONE) {
+ visibility.append(" speed=").append(accessPoint.getSpeedLabel());
+ }
+ visibility.append(String.format(" tx=%.1f,", info.txSuccessRate));
+ visibility.append(String.format("%.1f,", info.txRetriesRate));
+ visibility.append(String.format("%.1f ", info.txBadRate));
+ visibility.append(String.format("rx=%.1f", info.rxSuccessRate));
+ }
+
+ int maxRssi5 = WifiConfiguration.INVALID_RSSI;
+ int maxRssi24 = WifiConfiguration.INVALID_RSSI;
+ final int maxDisplayedScans = 4;
+ int num5 = 0; // number of scanned BSSID on 5GHz band
+ int num24 = 0; // number of scanned BSSID on 2.4Ghz band
+ int numBlackListed = 0;
+
+ // TODO: sort list by RSSI or age
+ long nowMs = SystemClock.elapsedRealtime();
+ for (ScanResult result : accessPoint.getScanResults().values()) {
+ if (result.frequency >= AccessPoint.LOWER_FREQ_5GHZ
+ && result.frequency <= AccessPoint.HIGHER_FREQ_5GHZ) {
+ // Strictly speaking: [4915, 5825]
+ num5++;
+
+ if (result.level > maxRssi5) {
+ maxRssi5 = result.level;
+ }
+ if (num5 <= maxDisplayedScans) {
+ scans5GHz.append(
+ verboseScanResultSummary(accessPoint, result, bssid,
+ nowMs));
+ }
+ } else if (result.frequency >= AccessPoint.LOWER_FREQ_24GHZ
+ && result.frequency <= AccessPoint.HIGHER_FREQ_24GHZ) {
+ // Strictly speaking: [2412, 2482]
+ num24++;
+
+ if (result.level > maxRssi24) {
+ maxRssi24 = result.level;
+ }
+ if (num24 <= maxDisplayedScans) {
+ scans24GHz.append(
+ verboseScanResultSummary(accessPoint, result, bssid,
+ nowMs));
+ }
+ }
+ }
+ visibility.append(" [");
+ if (num24 > 0) {
+ visibility.append("(").append(num24).append(")");
+ if (num24 > maxDisplayedScans) {
+ visibility.append("max=").append(maxRssi24).append(",");
+ }
+ visibility.append(scans24GHz.toString());
+ }
+ visibility.append(";");
+ if (num5 > 0) {
+ visibility.append("(").append(num5).append(")");
+ if (num5 > maxDisplayedScans) {
+ visibility.append("max=").append(maxRssi5).append(",");
+ }
+ visibility.append(scans5GHz.toString());
+ }
+ if (numBlackListed > 0) {
+ visibility.append("!").append(numBlackListed);
+ }
+ visibility.append("]");
+
+ return visibility.toString();
+ }
+
+ @VisibleForTesting
+ /* package */ static String verboseScanResultSummary(AccessPoint accessPoint, ScanResult result,
+ String bssid, long nowMs) {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append(" \n{").append(result.BSSID);
+ if (result.BSSID.equals(bssid)) {
+ stringBuilder.append("*");
+ }
+ stringBuilder.append("=").append(result.frequency);
+ stringBuilder.append(",").append(result.level);
+ int speed = getSpecificApSpeed(result, accessPoint.getScoredNetworkCache());
+ if (speed != AccessPoint.Speed.NONE) {
+ stringBuilder.append(",")
+ .append(accessPoint.getSpeedLabel(speed));
+ }
+ int ageSeconds = (int) (nowMs - result.timestamp / 1000) / 1000;
+ stringBuilder.append(",").append(ageSeconds).append("s");
+ stringBuilder.append("}");
+ return stringBuilder.toString();
+ }
+
+ @AccessPoint.Speed
+ private static int getSpecificApSpeed(ScanResult result,
+ Map<String, TimestampedScoredNetwork> scoredNetworkCache) {
+ TimestampedScoredNetwork timedScore = scoredNetworkCache.get(result.BSSID);
+ if (timedScore == null) {
+ return AccessPoint.Speed.NONE;
+ }
+ // For debugging purposes we may want to use mRssi rather than result.level as the average
+ // speed wil be determined by mRssi
+ return timedScore.getScore().calculateBadge(result.level);
+ }
+}
diff --git a/com/android/setupwizardlib/GlifLayoutTest.java b/com/android/setupwizardlib/GlifLayoutTest.java
index 967a52eb..360dfe22 100644
--- a/com/android/setupwizardlib/GlifLayoutTest.java
+++ b/com/android/setupwizardlib/GlifLayoutTest.java
@@ -54,7 +54,7 @@ import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class GlifLayoutTest {
private Context mContext;
diff --git a/com/android/setupwizardlib/items/ButtonItemDrawingTest.java b/com/android/setupwizardlib/items/ButtonItemDrawingTest.java
index 74d3be62..b97905c8 100644
--- a/com/android/setupwizardlib/items/ButtonItemDrawingTest.java
+++ b/com/android/setupwizardlib/items/ButtonItemDrawingTest.java
@@ -18,6 +18,7 @@ package com.android.setupwizardlib.items;
import static org.junit.Assert.assertTrue;
+import android.support.annotation.StyleRes;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.SmallTest;
import android.support.test.rule.UiThreadTestRule;
@@ -29,7 +30,6 @@ import android.widget.LinearLayout;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.test.util.DrawingTestHelper;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -38,40 +38,61 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class ButtonItemDrawingTest {
- private static final int GOOGLE_BLUE = 0xff4285f4;
+ private static final int GLIF_ACCENT_COLOR = 0xff4285f4;
+ private static final int GLIF_V3_ACCENT_COLOR = 0xff1a73e8;
// These tests need to be run on UI thread because button uses ValueAnimator
@Rule
public UiThreadTestRule mUiThreadTestRule = new UiThreadTestRule();
- private ViewGroup mParent;
+ @Test
+ @UiThreadTest
+ public void drawButton_glif_shouldHaveAccentColoredButton()
+ throws InstantiationException, IllegalAccessException {
+ Button button = createButton(R.style.SuwThemeGlif_Light);
+
+ DrawingTestHelper drawingTestHelper = new DrawingTestHelper(50, 50);
+ drawingTestHelper.drawView(button);
- @Before
- public void setUp() throws Exception {
- mParent = new LinearLayout(
- DrawingTestHelper.createCanvasActivity(R.style.SuwThemeGlif_Light));
+ int accentPixelCount =
+ countPixelsWithColor(drawingTestHelper.getPixels(), GLIF_ACCENT_COLOR);
+ assertTrue("> 10 pixels should be #4285f4. Found " + accentPixelCount,
+ accentPixelCount > 10);
}
@Test
@UiThreadTest
- public void testColoredButtonTheme() {
+ public void drawButton_glifV3_shouldHaveAccentColoredButton()
+ throws InstantiationException, IllegalAccessException {
+ Button button = createButton(R.style.SuwThemeGlifV3_Light);
+
+ DrawingTestHelper drawingTestHelper = new DrawingTestHelper(50, 50);
+ drawingTestHelper.drawView(button);
+
+ int accentPixelCount =
+ countPixelsWithColor(drawingTestHelper.getPixels(), GLIF_V3_ACCENT_COLOR);
+ assertTrue("> 10 pixels should be #1a73e8. Found " + accentPixelCount,
+ accentPixelCount > 10);
+ }
+
+ private Button createButton(@StyleRes int theme)
+ throws InstantiationException, IllegalAccessException {
+ final ViewGroup parent = new LinearLayout(DrawingTestHelper.createCanvasActivity(theme));
TestButtonItem item = new TestButtonItem();
item.setTheme(R.style.SuwButtonItem_Colored);
item.setText("foobar");
- final Button button = item.createButton(mParent);
-
- DrawingTestHelper drawingTestHelper = new DrawingTestHelper(50, 50);
- drawingTestHelper.drawView(button);
+ return item.createButton(parent);
+ }
- int googleBluePixelCount = 0;
- for (int pixel : drawingTestHelper.getPixels()) {
- if (pixel == GOOGLE_BLUE) {
- googleBluePixelCount++;
+ private int countPixelsWithColor(int[] pixels, int color) {
+ int count = 0;
+ for (int pixel : pixels) {
+ if (pixel == color) {
+ count++;
}
}
- assertTrue("> 10 pixels should be Google blue. Found " + googleBluePixelCount,
- googleBluePixelCount > 10);
+ return count;
}
private static class TestButtonItem extends ButtonItem {
diff --git a/com/android/setupwizardlib/items/ButtonItemTest.java b/com/android/setupwizardlib/items/ButtonItemTest.java
index 93b9a6d6..40e5da82 100644
--- a/com/android/setupwizardlib/items/ButtonItemTest.java
+++ b/com/android/setupwizardlib/items/ButtonItemTest.java
@@ -39,7 +39,6 @@ import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.items.ButtonItem.OnClickListener;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -50,9 +49,7 @@ import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(
- constants = BuildConfig.class,
- sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class ButtonItemTest {
private ViewGroup mParent;
diff --git a/com/android/setupwizardlib/items/ExpandableSwitchItemTest.java b/com/android/setupwizardlib/items/ExpandableSwitchItemTest.java
index 4fb3e282..3020ed30 100644
--- a/com/android/setupwizardlib/items/ExpandableSwitchItemTest.java
+++ b/com/android/setupwizardlib/items/ExpandableSwitchItemTest.java
@@ -32,7 +32,6 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -44,7 +43,7 @@ import org.robolectric.annotation.Config;
import java.util.ArrayList;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class ExpandableSwitchItemTest {
private TextView mSummaryView;
diff --git a/com/android/setupwizardlib/items/ItemGroupTest.java b/com/android/setupwizardlib/items/ItemGroupTest.java
index a61b750b..ecaec71d 100644
--- a/com/android/setupwizardlib/items/ItemGroupTest.java
+++ b/com/android/setupwizardlib/items/ItemGroupTest.java
@@ -24,7 +24,6 @@ import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Before;
@@ -36,9 +35,7 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(
- constants = BuildConfig.class,
- sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class ItemGroupTest {
private static final Item CHILD_1 = new EqualsItem("Child 1");
diff --git a/com/android/setupwizardlib/items/SwitchItemTest.java b/com/android/setupwizardlib/items/SwitchItemTest.java
index d391d802..fa5bbba9 100644
--- a/com/android/setupwizardlib/items/SwitchItemTest.java
+++ b/com/android/setupwizardlib/items/SwitchItemTest.java
@@ -31,7 +31,6 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -40,7 +39,7 @@ import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class SwitchItemTest {
private SwitchCompat mSwitch;
diff --git a/com/android/setupwizardlib/robolectric/PatchedGradleManifestFactory.java b/com/android/setupwizardlib/robolectric/PatchedGradleManifestFactory.java
deleted file mode 100644
index 64c63e71..00000000
--- a/com/android/setupwizardlib/robolectric/PatchedGradleManifestFactory.java
+++ /dev/null
@@ -1,126 +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 com.android.setupwizardlib.robolectric;
-
-import org.robolectric.annotation.Config;
-import org.robolectric.internal.GradleManifestFactory;
-import org.robolectric.internal.ManifestIdentifier;
-import org.robolectric.res.FileFsFile;
-import org.robolectric.util.Logger;
-import org.robolectric.util.ReflectionHelpers;
-
-import java.io.File;
-import java.net.URL;
-
-/**
- * Modified GradleManifestFactory to patch an issue where some build variants have merged
- * resources under res/merged/variant/type while others have it under bundles/variant/type/res.
- *
- * The change is that in the .exists() checks below we check for specific folders for the build
- * variant rather than checking existence at the parent directory.
- */
-class PatchedGradleManifestFactory extends GradleManifestFactory {
-
- @Override
- public ManifestIdentifier identify(Config config) {
- if (config.constants() == Void.class) {
- Logger.error("Field 'constants' not specified in @Config annotation");
- Logger.error("This is required when using Robolectric with Gradle!");
- throw new RuntimeException("No 'constants' field in @Config annotation!");
- }
-
- final String buildOutputDir = getBuildOutputDir(config);
- final String type = getType(config);
- final String flavor = getFlavor(config);
- final String abiSplit = getAbiSplit(config);
- final String packageName = config.packageName().isEmpty()
- ? config.constants().getPackage().getName()
- : config.packageName();
-
- final FileFsFile res;
- final FileFsFile assets;
- final FileFsFile manifest;
-
- if (FileFsFile.from(buildOutputDir, "data-binding-layout-out", flavor, type).exists()) {
- // Android gradle plugin 1.5.0+ puts the merged layouts in data-binding-layout-out.
- // https://github.com/robolectric/robolectric/issues/2143
- res = FileFsFile.from(buildOutputDir, "data-binding-layout-out", flavor, type);
- } else if (FileFsFile.from(buildOutputDir, "res", "merged", flavor, type).exists()) {
- // res/merged added in Android Gradle plugin 1.3-beta1
- res = FileFsFile.from(buildOutputDir, "res", "merged", flavor, type);
- } else if (FileFsFile.from(buildOutputDir, "res", flavor, type).exists()) {
- res = FileFsFile.from(buildOutputDir, "res", flavor, type);
- } else {
- res = FileFsFile.from(buildOutputDir, "bundles", flavor, type, "res");
- }
-
- if (FileFsFile.from(buildOutputDir, "assets", flavor, type).exists()) {
- assets = FileFsFile.from(buildOutputDir, "assets", flavor, type);
- } else {
- assets = FileFsFile.from(buildOutputDir, "bundles", flavor, type, "assets");
- }
-
- String manifestName = config.manifest();
- URL manifestUrl = getClass().getClassLoader().getResource(manifestName);
- if (manifestUrl != null && manifestUrl.getProtocol().equals("file")) {
- manifest = FileFsFile.from(manifestUrl.getPath());
- } else if (FileFsFile.from(buildOutputDir, "manifests", "full", flavor, abiSplit, type,
- manifestName).exists()) {
- manifest = FileFsFile.from(
- buildOutputDir, "manifests", "full", flavor, abiSplit, type, manifestName);
- } else if (FileFsFile.from(buildOutputDir, "manifests", "aapt", flavor, abiSplit, type,
- manifestName).exists()) {
- // Android gradle plugin 2.2.0+ can put library manifest files inside of "aapt"
- // instead of "full"
- manifest = FileFsFile.from(buildOutputDir, "manifests", "aapt", flavor, abiSplit,
- type, manifestName);
- } else {
- manifest = FileFsFile.from(buildOutputDir, "bundles", flavor, abiSplit, type,
- manifestName);
- }
-
- return new ManifestIdentifier(manifest, res, assets, packageName, null);
- }
-
- private static String getBuildOutputDir(Config config) {
- return config.buildDir() + File.separator + "intermediates";
- }
-
- private static String getType(Config config) {
- try {
- return ReflectionHelpers.getStaticField(config.constants(), "BUILD_TYPE");
- } catch (Throwable e) {
- return null;
- }
- }
-
- private static String getFlavor(Config config) {
- try {
- return ReflectionHelpers.getStaticField(config.constants(), "FLAVOR");
- } catch (Throwable e) {
- return null;
- }
- }
-
- private static String getAbiSplit(Config config) {
- try {
- return config.abiSplit();
- } catch (Throwable e) {
- return null;
- }
- }
-}
diff --git a/com/android/setupwizardlib/robolectric/SuwLibRobolectricTestRunner.java b/com/android/setupwizardlib/robolectric/SuwLibRobolectricTestRunner.java
index 509201a8..61baa232 100644
--- a/com/android/setupwizardlib/robolectric/SuwLibRobolectricTestRunner.java
+++ b/com/android/setupwizardlib/robolectric/SuwLibRobolectricTestRunner.java
@@ -20,42 +20,13 @@ import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.internal.ManifestFactory;
public class SuwLibRobolectricTestRunner extends RobolectricTestRunner {
- private String mModuleRootPath;
-
public SuwLibRobolectricTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
- // Hack to determine the module root path in the build folder (e.g. out/gradle/setup-wizard-lib)
- private void updateModuleRootPath(Config config) {
- String moduleRoot = config.constants().getResource("").toString()
- .replace("file:", "").replace("jar:", "");
- mModuleRootPath =
- moduleRoot.substring(0, moduleRoot.lastIndexOf("/build")) + "/setup-wizard-lib";
- }
-
- /**
- * Return the default config used to run Robolectric tests.
- */
- @Override
- protected Config buildGlobalConfig() {
- Config parent = super.buildGlobalConfig();
- updateModuleRootPath(parent);
- return new Config.Builder(parent)
- .setBuildDir(mModuleRootPath + "/build")
- .build();
- }
-
- @Override
- protected ManifestFactory getManifestFactory(Config config) {
- return new PatchedGradleManifestFactory();
- }
-
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
System.out.println("===== Running " + method + " =====");
diff --git a/com/android/setupwizardlib/span/LinkSpanTest.java b/com/android/setupwizardlib/span/LinkSpanTest.java
index f86e057a..fe72e039 100644
--- a/com/android/setupwizardlib/span/LinkSpanTest.java
+++ b/com/android/setupwizardlib/span/LinkSpanTest.java
@@ -23,15 +23,12 @@ import android.content.Context;
import android.content.ContextWrapper;
import android.widget.TextView;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class)
public class LinkSpanTest {
@Test
@@ -68,6 +65,7 @@ public class LinkSpanTest {
assertSame("Clicked LinkSpan should be passed to setup", linkSpan, context.clickedSpan);
}
+ @SuppressWarnings("deprecation")
private static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {
public LinkSpan clickedSpan = null;
diff --git a/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java b/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java
index fa81dc05..ec3622de 100644
--- a/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java
+++ b/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java
@@ -31,7 +31,6 @@ import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.ListView;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Before;
@@ -42,7 +41,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@RunWith(SuwLibRobolectricTestRunner.class)
public class ListViewScrollHandlingDelegateTest {
diff --git a/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java b/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java
index b5093897..5912f7f1 100644
--- a/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java
+++ b/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java
@@ -27,7 +27,6 @@ import static org.robolectric.RuntimeEnvironment.application;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnScrollListener;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Before;
@@ -38,7 +37,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@RunWith(SuwLibRobolectricTestRunner.class)
public class RecyclerViewScrollHandlingDelegateTest {
diff --git a/com/android/setupwizardlib/template/RequireScrollMixinTest.java b/com/android/setupwizardlib/template/RequireScrollMixinTest.java
index 8e39c439..c641449e 100644
--- a/com/android/setupwizardlib/template/RequireScrollMixinTest.java
+++ b/com/android/setupwizardlib/template/RequireScrollMixinTest.java
@@ -34,7 +34,6 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.TemplateLayout;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.template.RequireScrollMixin.OnRequireScrollStateChangedListener;
@@ -48,7 +47,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@RunWith(SuwLibRobolectricTestRunner.class)
public class RequireScrollMixinTest {
diff --git a/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java b/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java
index f77e2569..429445c5 100644
--- a/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java
+++ b/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java
@@ -23,7 +23,6 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.robolectric.RuntimeEnvironment.application;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.view.BottomScrollView;
import com.android.setupwizardlib.view.BottomScrollView.BottomScrollListener;
@@ -36,7 +35,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
@RunWith(SuwLibRobolectricTestRunner.class)
public class ScrollViewScrollHandlingDelegateTest {
diff --git a/com/android/setupwizardlib/util/DimensionConsistencyTest.java b/com/android/setupwizardlib/util/DimensionConsistencyTest.java
index 43e7f032..7a08235c 100644
--- a/com/android/setupwizardlib/util/DimensionConsistencyTest.java
+++ b/com/android/setupwizardlib/util/DimensionConsistencyTest.java
@@ -25,7 +25,6 @@ import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -35,7 +34,7 @@ import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = Config.ALL_SDKS)
+@Config(sdk = Config.ALL_SDKS)
public class DimensionConsistencyTest {
// Visual height of the framework switch widget
diff --git a/com/android/setupwizardlib/util/GlifDimensionTest.java b/com/android/setupwizardlib/util/GlifDimensionTest.java
index 2be64e1d..c10c1225 100644
--- a/com/android/setupwizardlib/util/GlifDimensionTest.java
+++ b/com/android/setupwizardlib/util/GlifDimensionTest.java
@@ -26,7 +26,6 @@ import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -36,7 +35,7 @@ import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = Config.ALL_SDKS)
+@Config(sdk = Config.ALL_SDKS)
public class GlifDimensionTest {
private Context mContext;
diff --git a/com/android/setupwizardlib/util/GlifStyleTest.java b/com/android/setupwizardlib/util/GlifStyleTest.java
index aea2c03f..20661ff0 100644
--- a/com/android/setupwizardlib/util/GlifStyleTest.java
+++ b/com/android/setupwizardlib/util/GlifStyleTest.java
@@ -18,6 +18,7 @@ package com.android.setupwizardlib.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.robolectric.RuntimeEnvironment.application;
import android.annotation.TargetApi;
@@ -29,8 +30,8 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.ContextThemeWrapper;
import android.widget.Button;
+import android.widget.ProgressBar;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -41,7 +42,7 @@ import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
+@Config(sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
public class GlifStyleTest {
private Context mContext;
@@ -76,6 +77,15 @@ public class GlifStyleTest {
assertEquals(0x00000000, activity.getWindow().getStatusBarColor());
}
+ @Test
+ public void glifLoadingScreen_shouldHaveProgressBar() {
+ GlifThemeActivity activity = Robolectric.setupActivity(GlifThemeActivity.class);
+ activity.setContentView(R.layout.suw_glif_loading_screen);
+
+ assertTrue("Progress bar should exist",
+ activity.findViewById(R.id.suw_large_progress_bar) instanceof ProgressBar);
+ }
+
private static class GlifThemeActivity extends Activity {
@Override
diff --git a/com/android/setupwizardlib/util/LinkAccessibilityHelper.java b/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
index 1fb3a373..9965aa01 100644
--- a/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
+++ b/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
@@ -152,6 +152,7 @@ public class LinkAccessibilityHelper extends AccessibilityDelegateCompat {
mView = view;
}
+ @Override
protected int getVirtualViewAt(float x, float y) {
final CharSequence text = mView.getText();
if (text instanceof Spanned) {
@@ -167,6 +168,7 @@ public class LinkAccessibilityHelper extends AccessibilityDelegateCompat {
return ExploreByTouchHelper.INVALID_ID;
}
+ @Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
final CharSequence text = mView.getText();
if (text instanceof Spanned) {
@@ -179,6 +181,7 @@ public class LinkAccessibilityHelper extends AccessibilityDelegateCompat {
}
}
+ @Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
final ClickableSpan span = getSpanForOffset(virtualViewId);
if (span != null) {
@@ -189,6 +192,7 @@ public class LinkAccessibilityHelper extends AccessibilityDelegateCompat {
}
}
+ @Override
protected void onPopulateNodeForVirtualView(
int virtualViewId,
AccessibilityNodeInfoCompat info) {
@@ -210,6 +214,7 @@ public class LinkAccessibilityHelper extends AccessibilityDelegateCompat {
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
}
+ @Override
protected boolean onPerformActionForVirtualView(
int virtualViewId,
int action,
diff --git a/com/android/setupwizardlib/util/PartnerTest.java b/com/android/setupwizardlib/util/PartnerTest.java
index aeb678fa..683e40b5 100644
--- a/com/android/setupwizardlib/util/PartnerTest.java
+++ b/com/android/setupwizardlib/util/PartnerTest.java
@@ -37,7 +37,6 @@ import android.content.res.Resources;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.util.Partner.ResourceEntry;
@@ -57,7 +56,6 @@ import java.util.Collections;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(
- constants = BuildConfig.class,
sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK },
shadows = ShadowApplicationPackageManager.class)
public class PartnerTest {
diff --git a/com/android/setupwizardlib/util/WizardManagerHelper.java b/com/android/setupwizardlib/util/WizardManagerHelper.java
index 32929aad..cf9ddac0 100644
--- a/com/android/setupwizardlib/util/WizardManagerHelper.java
+++ b/com/android/setupwizardlib/util/WizardManagerHelper.java
@@ -80,22 +80,22 @@ public class WizardManagerHelper {
public static final String THEME_GLIF_V2 = "glif_v2";
/**
- * @deprecated Use {@link #THEME_GLIF_V2} instead.
- */
- @Deprecated
- public static final String THEME_GLIF_PIXEL = THEME_GLIF_V2;
-
- /**
* Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the default theme used in
* setup wizard for O DR.
*/
public static final String THEME_GLIF_V2_LIGHT = "glif_v2_light";
/**
- * @deprecated Use {@link #THEME_GLIF_V2_LIGHT} instead.
+ * Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the dark variant of the
+ * theme used in setup wizard for P.
+ */
+ public static final String THEME_GLIF_V3 = "glif_v3";
+
+ /**
+ * Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the default theme used in
+ * setup wizard for P.
*/
- @Deprecated
- public static final String THEME_GLIF_PIXEL_LIGHT = THEME_GLIF_V2_LIGHT;
+ public static final String THEME_GLIF_V3_LIGHT = "glif_v3_light";
/**
* Get an intent that will invoke the next step of setup wizard.
@@ -253,10 +253,12 @@ public class WizardManagerHelper {
*/
public static boolean isLightTheme(String theme, boolean def) {
if (THEME_HOLO_LIGHT.equals(theme) || THEME_MATERIAL_LIGHT.equals(theme)
- || THEME_GLIF_LIGHT.equals(theme) || THEME_GLIF_V2_LIGHT.equals(theme)) {
+ || THEME_GLIF_LIGHT.equals(theme) || THEME_GLIF_V2_LIGHT.equals(theme)
+ || THEME_GLIF_V3_LIGHT.equals(theme)) {
return true;
} else if (THEME_HOLO.equals(theme) || THEME_MATERIAL.equals(theme)
- || THEME_GLIF.equals(theme) || THEME_GLIF_V2.equals(theme)) {
+ || THEME_GLIF.equals(theme) || THEME_GLIF_V2.equals(theme)
+ || THEME_GLIF_V3.equals(theme)) {
return false;
} else {
return def;
@@ -301,6 +303,10 @@ public class WizardManagerHelper {
public static @StyleRes int getThemeRes(String theme, @StyleRes int defaultTheme) {
if (theme != null) {
switch (theme) {
+ case THEME_GLIF_V3_LIGHT:
+ return R.style.SuwThemeGlifV3_Light;
+ case THEME_GLIF_V3:
+ return R.style.SuwThemeGlifV3;
case THEME_GLIF_V2_LIGHT:
return R.style.SuwThemeGlifV2_Light;
case THEME_GLIF_V2:
diff --git a/com/android/setupwizardlib/util/WizardManagerHelperTest.java b/com/android/setupwizardlib/util/WizardManagerHelperTest.java
index c236bb54..0d15ef41 100644
--- a/com/android/setupwizardlib/util/WizardManagerHelperTest.java
+++ b/com/android/setupwizardlib/util/WizardManagerHelperTest.java
@@ -31,7 +31,6 @@ import android.provider.Settings.Global;
import android.provider.Settings.Secure;
import android.support.annotation.StyleRes;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -39,8 +38,12 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = Config.NEWEST_SDK)
+@Config(sdk = Config.NEWEST_SDK)
public class WizardManagerHelperTest {
@Test
@@ -104,75 +107,57 @@ public class WizardManagerHelperTest {
}
@Test
- public void testHoloIsNotLightTheme() {
- final Intent intent = new Intent();
- intent.putExtra("theme", "holo");
- assertFalse("Theme holo should not be light theme",
- WizardManagerHelper.isLightTheme(intent, true));
- }
-
- @Test
- public void testHoloLightIsLightTheme() {
- final Intent intent = new Intent();
- intent.putExtra("theme", "holo_light");
- assertTrue("Theme holo_light should be light theme",
- WizardManagerHelper.isLightTheme(intent, false));
- }
-
- @Test
- public void testMaterialIsNotLightTheme() {
- final Intent intent = new Intent();
- intent.putExtra("theme", "material");
- assertFalse("Theme material should not be light theme",
- WizardManagerHelper.isLightTheme(intent, true));
- }
-
- @Test
- public void testMaterialLightIsLightTheme() {
- final Intent intent = new Intent();
- intent.putExtra("theme", "material_light");
- assertTrue("Theme material_light should be light theme",
- WizardManagerHelper.isLightTheme(intent, false));
- }
-
- @Test
- public void testGlifIsDarkTheme() {
- final Intent intent = new Intent();
- intent.putExtra("theme", "glif");
- assertFalse("Theme glif should be dark theme",
- WizardManagerHelper.isLightTheme(intent, false));
- assertFalse("Theme glif should be dark theme",
- WizardManagerHelper.isLightTheme(intent, true));
- }
-
- @Test
- public void testGlifLightIsLightTheme() {
- final Intent intent = new Intent();
- intent.putExtra("theme", "glif_light");
- assertTrue("Theme glif_light should be light theme",
- WizardManagerHelper.isLightTheme(intent, false));
- assertTrue("Theme glif_light should be light theme",
- WizardManagerHelper.isLightTheme(intent, true));
- }
-
- @Test
- public void testGlifV2IsDarkTheme() {
- final Intent intent = new Intent();
- intent.putExtra("theme", "glif_v2");
- assertFalse("Theme glif_v2 should be dark theme",
- WizardManagerHelper.isLightTheme(intent, false));
- assertFalse("Theme glif_v2 should be dark theme",
- WizardManagerHelper.isLightTheme(intent, true));
+ public void isLightTheme_shouldReturnTrue_whenThemeIsLight() {
+ List<String> lightThemes = Arrays.asList(
+ "holo_light",
+ "material_light",
+ "glif_light",
+ "glif_v2_light",
+ "glif_v3_light"
+ );
+ ArrayList<String> unexpectedIntentThemes = new ArrayList<>();
+ ArrayList<String> unexpectedStringThemes = new ArrayList<>();
+ for (final String theme : lightThemes) {
+ Intent intent = new Intent();
+ intent.putExtra(WizardManagerHelper.EXTRA_THEME, theme);
+ if (!WizardManagerHelper.isLightTheme(intent, false)) {
+ unexpectedIntentThemes.add(theme);
+ }
+ if (!WizardManagerHelper.isLightTheme(theme, false)) {
+ unexpectedStringThemes.add(theme);
+ }
+ }
+ assertTrue("Intent themes " + unexpectedIntentThemes + " should be light",
+ unexpectedIntentThemes.isEmpty());
+ assertTrue("String themes " + unexpectedStringThemes + " should be light",
+ unexpectedStringThemes.isEmpty());
}
@Test
- public void testGlifV2LightIsLightTheme() {
- final Intent intent = new Intent();
- intent.putExtra("theme", "glif_v2_light");
- assertTrue("Theme glif_v2_light should be light theme",
- WizardManagerHelper.isLightTheme(intent, false));
- assertTrue("Theme glif_v2_light should be light theme",
- WizardManagerHelper.isLightTheme(intent, true));
+ public void isLightTheme_shouldReturnFalse_whenThemeIsNotLight() {
+ List<String> lightThemes = Arrays.asList(
+ "holo",
+ "material",
+ "glif",
+ "glif_v2",
+ "glif_v3"
+ );
+ ArrayList<String> unexpectedIntentThemes = new ArrayList<>();
+ ArrayList<String> unexpectedStringThemes = new ArrayList<>();
+ for (final String theme : lightThemes) {
+ Intent intent = new Intent();
+ intent.putExtra(WizardManagerHelper.EXTRA_THEME, theme);
+ if (WizardManagerHelper.isLightTheme(intent, true)) {
+ unexpectedIntentThemes.add(theme);
+ }
+ if (WizardManagerHelper.isLightTheme(theme, true)) {
+ unexpectedStringThemes.add(theme);
+ }
+ }
+ assertTrue("Intent themes " + unexpectedIntentThemes + " should not be light",
+ unexpectedIntentThemes.isEmpty());
+ assertTrue("String themes " + unexpectedStringThemes + " should not be light",
+ unexpectedStringThemes.isEmpty());
}
@Test
@@ -195,19 +180,15 @@ public class WizardManagerHelperTest {
}
@Test
- public void testIsLightThemeString() {
- assertTrue("isLightTheme should return true for material_light",
- WizardManagerHelper.isLightTheme("material_light", false));
- assertFalse("isLightTheme should return false for material",
- WizardManagerHelper.isLightTheme("material", false));
- assertTrue("isLightTheme should return true for holo_light",
- WizardManagerHelper.isLightTheme("holo_light", false));
- assertFalse("isLightTheme should return false for holo",
- WizardManagerHelper.isLightTheme("holo", false));
- assertTrue("isLightTheme should return default value true",
- WizardManagerHelper.isLightTheme("abracadabra", true));
- assertFalse("isLightTheme should return default value false",
- WizardManagerHelper.isLightTheme("abracadabra", false));
+ public void testGetThemeResGlifV3Light() {
+ assertEquals(R.style.SuwThemeGlifV3_Light,
+ WizardManagerHelper.getThemeRes("glif_v3_light", 0));
+ }
+
+ @Test
+ public void testGetThemeResGlifV3() {
+ assertEquals(R.style.SuwThemeGlifV3,
+ WizardManagerHelper.getThemeRes("glif_v3", 0));
}
@Test
diff --git a/com/android/setupwizardlib/view/FillContentLayoutTest.java b/com/android/setupwizardlib/view/FillContentLayoutTest.java
index f1332c0f..ae4f3d1e 100644
--- a/com/android/setupwizardlib/view/FillContentLayoutTest.java
+++ b/com/android/setupwizardlib/view/FillContentLayoutTest.java
@@ -22,7 +22,6 @@ import static org.robolectric.RuntimeEnvironment.application;
import android.view.View;
import android.view.View.MeasureSpec;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import org.junit.Test;
@@ -31,7 +30,7 @@ import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
@RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
+@Config(sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
public class FillContentLayoutTest {
@Test
diff --git a/com/android/setupwizardlib/view/IllustrationVideoViewTest.java b/com/android/setupwizardlib/view/IllustrationVideoViewTest.java
index ddf59ca4..6db88527 100644
--- a/com/android/setupwizardlib/view/IllustrationVideoViewTest.java
+++ b/com/android/setupwizardlib/view/IllustrationVideoViewTest.java
@@ -31,7 +31,6 @@ import android.os.Build.VERSION_CODES;
import android.support.annotation.RawRes;
import android.view.Surface;
-import com.android.setupwizardlib.BuildConfig;
import com.android.setupwizardlib.R;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.view.IllustrationVideoViewTest.ShadowMockMediaPlayer;
@@ -54,7 +53,6 @@ import org.robolectric.util.ReflectionHelpers;
@RunWith(SuwLibRobolectricTestRunner.class)
@Config(
- constants = BuildConfig.class,
sdk = Config.NEWEST_SDK,
shadows = {
ShadowMockMediaPlayer.class,
diff --git a/com/android/setupwizardlib/view/NavigationBarButton.java b/com/android/setupwizardlib/view/NavigationBarButton.java
index 5172c476..d7a3c2e5 100644
--- a/com/android/setupwizardlib/view/NavigationBarButton.java
+++ b/com/android/setupwizardlib/view/NavigationBarButton.java
@@ -16,6 +16,7 @@
package com.android.setupwizardlib.view;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
@@ -30,6 +31,7 @@ import android.widget.Button;
* Button for navigation bar, which includes tinting of its compound drawables to be used for dark
* and light themes.
*/
+@SuppressLint("AppCompatCustomView")
public class NavigationBarButton extends Button {
public NavigationBarButton(Context context) {
diff --git a/com/android/shell/Screenshooter.java b/com/android/shell/Screenshooter.java
index 8e27edf9..8e016196 100644
--- a/com/android/shell/Screenshooter.java
+++ b/com/android/shell/Screenshooter.java
@@ -17,12 +17,11 @@
package com.android.shell;
import android.graphics.Bitmap;
-import android.graphics.Canvas;
import android.graphics.Point;
+import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.util.Log;
import android.view.Display;
-import android.view.Surface;
import android.view.SurfaceControl;
/**
@@ -35,18 +34,6 @@ final class Screenshooter {
private static final String TAG = "Screenshooter";
- /** Rotation constant: Freeze rotation to 0 degrees (natural orientation) */
- public static final int ROTATION_FREEZE_0 = Surface.ROTATION_0;
-
- /** Rotation constant: Freeze rotation to 90 degrees . */
- public static final int ROTATION_FREEZE_90 = Surface.ROTATION_90;
-
- /** Rotation constant: Freeze rotation to 180 degrees . */
- public static final int ROTATION_FREEZE_180 = Surface.ROTATION_180;
-
- /** Rotation constant: Freeze rotation to 270 degrees . */
- public static final int ROTATION_FREEZE_270 = Surface.ROTATION_270;
-
/**
* Takes a screenshot.
*
@@ -60,78 +47,21 @@ final class Screenshooter {
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();
+ Rect crop = new Rect(0, 0, displayWidth, displayHeight);
Log.d(TAG, "Taking screenshot of dimensions " + displayWidth + " x " + displayHeight);
// Take the screenshot
Bitmap screenShot =
- SurfaceControl.screenshot((int) screenshotWidth, (int) screenshotHeight);
+ SurfaceControl.screenshot(crop, displayWidth, displayHeight, rotation);
if (screenShot == null) {
- Log.e(TAG, "Failed to take screenshot of dimensions " + screenshotWidth + " x "
- + screenshotHeight);
+ Log.e(TAG, "Failed to take screenshot of dimensions " + displayWidth + " x "
+ + displayHeight);
return null;
}
- // Rotate the screenshot to the current orientation
- if (rotation != ROTATION_FREEZE_0) {
- Bitmap unrotatedScreenShot = Bitmap.createBitmap(displayWidth, displayHeight,
- Bitmap.Config.ARGB_8888, screenShot.hasAlpha(), screenShot.getColorSpace());
- 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);
return screenShot;
}
-
- private static float getDegreesForRotation(int value) {
- switch (value) {
- case Surface.ROTATION_90: {
- return 360f - 90f;
- }
- case Surface.ROTATION_180: {
- return 360f - 180f;
- }
- case Surface.ROTATION_270: {
- return 360f - 270f;
- } default: {
- return 0;
- }
- }
- }
-
}
diff --git a/com/android/systemui/EmulatedDisplayCutout.java b/com/android/systemui/EmulatedDisplayCutout.java
new file mode 100644
index 00000000..6aa465ce
--- /dev/null
+++ b/com/android/systemui/EmulatedDisplayCutout.java
@@ -0,0 +1,136 @@
+/*
+ * 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 com.android.systemui;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Region;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.view.DisplayCutout;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Emulates a display cutout by drawing its shape in an overlay as supplied by
+ * {@link DisplayCutout}.
+ */
+public class EmulatedDisplayCutout extends SystemUI {
+ private View mOverlay;
+ private boolean mAttached;
+ private WindowManager mWindowManager;
+
+ @Override
+ public void start() {
+ mWindowManager = mContext.getSystemService(WindowManager.class);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.EMULATE_DISPLAY_CUTOUT),
+ false, mObserver, UserHandle.USER_ALL);
+ mObserver.onChange(false);
+ }
+
+ private void setAttached(boolean attached) {
+ if (attached && !mAttached) {
+ if (mOverlay == null) {
+ mOverlay = new CutoutView(mContext);
+ mOverlay.setLayoutParams(getLayoutParams());
+ }
+ mWindowManager.addView(mOverlay, mOverlay.getLayoutParams());
+ mAttached = true;
+ } else if (!attached && mAttached) {
+ mWindowManager.removeView(mOverlay);
+ mAttached = false;
+ }
+ }
+
+ private WindowManager.LayoutParams getLayoutParams() {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+ | WindowManager.LayoutParams.FLAG_SLIPPERY
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR,
+ PixelFormat.TRANSLUCENT);
+ lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS
+ | WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
+ lp.flags2 |= WindowManager.LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA;
+ lp.setTitle("EmulatedDisplayCutout");
+ lp.gravity = Gravity.TOP;
+ return lp;
+ }
+
+ private ContentObserver mObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
+ @Override
+ public void onChange(boolean selfChange) {
+ boolean emulateCutout = Settings.Global.getInt(
+ mContext.getContentResolver(), Settings.Global.EMULATE_DISPLAY_CUTOUT,
+ Settings.Global.EMULATE_DISPLAY_CUTOUT_OFF)
+ != Settings.Global.EMULATE_DISPLAY_CUTOUT_OFF;
+ setAttached(emulateCutout);
+ }
+ };
+
+ private static class CutoutView extends View {
+ private final Paint mPaint = new Paint();
+ private final Path mBounds = new Path();
+
+ CutoutView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ if (insets.getDisplayCutout() != null) {
+ insets.getDisplayCutout().getBounds().getBoundaryPath(mBounds);
+ } else {
+ mBounds.reset();
+ }
+ invalidate();
+ return insets.consumeDisplayCutout();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (!mBounds.isEmpty()) {
+ mPaint.setColor(Color.DKGRAY);
+ mPaint.setStyle(Paint.Style.FILL);
+
+ canvas.drawPath(mBounds, mPaint);
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/LatencyTester.java b/com/android/systemui/LatencyTester.java
index 1d55ee5a..cbb69ee9 100644
--- a/com/android/systemui/LatencyTester.java
+++ b/com/android/systemui/LatencyTester.java
@@ -25,7 +25,7 @@ import android.os.PowerManager;
import android.os.SystemClock;
import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.LatencyTracker;
+import com.android.internal.util.LatencyTracker;
import com.android.systemui.statusbar.phone.FingerprintUnlockController;
import com.android.systemui.statusbar.phone.StatusBar;
diff --git a/com/android/systemui/SystemUIApplication.java b/com/android/systemui/SystemUIApplication.java
index 9adafda7..35383271 100644
--- a/com/android/systemui/SystemUIApplication.java
+++ b/com/android/systemui/SystemUIApplication.java
@@ -37,9 +37,7 @@ import com.android.systemui.keyboard.KeyboardUI;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.media.RingtonePlayer;
import com.android.systemui.pip.PipUI;
-import com.android.systemui.plugins.GlobalActions;
import com.android.systemui.plugins.OverlayPlugin;
-import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.power.PowerUI;
@@ -54,6 +52,7 @@ import com.android.systemui.util.NotificationChannels;
import com.android.systemui.util.leak.GarbageMonitor;
import com.android.systemui.volume.VolumeUI;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -66,44 +65,9 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
private static final boolean DEBUG = false;
/**
- * The classes of the stuff to start.
- */
- private final Class<?>[] SERVICES = new Class[] {
- Dependency.class,
- NotificationChannels.class,
- CommandQueue.CommandQueueStart.class,
- KeyguardViewMediator.class,
- Recents.class,
- VolumeUI.class,
- Divider.class,
- SystemBars.class,
- StorageNotification.class,
- PowerUI.class,
- RingtonePlayer.class,
- KeyboardUI.class,
- PipUI.class,
- ShortcutKeyDispatcher.class,
- VendorServices.class,
- GarbageMonitor.Service.class,
- LatencyTester.class,
- GlobalActionsComponent.class,
- RoundedCorners.class,
- };
-
- /**
- * The classes of the stuff to start for each user. This is a subset of the services listed
- * above.
- */
- private final Class<?>[] SERVICES_PER_USER = new Class[] {
- Dependency.class,
- NotificationChannels.class,
- Recents.class
- };
-
- /**
* Hold a reference on the stuff we start.
*/
- private final SystemUI[] mServices = new SystemUI[SERVICES.length];
+ private SystemUI[] mServices;
private boolean mServicesStarted;
private boolean mBootCompleted;
private final Map<Class<?>, Object> mComponents = new HashMap<>();
@@ -149,7 +113,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
// been broadcasted on startup for the primary SystemUI process. Instead, for
// components which require the SystemUI component to be initialized per-user, we
// start those components now for the current non-system user.
- startServicesIfNeeded(SERVICES_PER_USER);
+ startSecondaryUserServicesIfNeeded();
}
}
@@ -161,7 +125,8 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
*/
public void startServicesIfNeeded() {
- startServicesIfNeeded(SERVICES);
+ String[] names = getResources().getStringArray(R.array.config_systemUIServiceComponents);
+ startServicesIfNeeded(names);
}
/**
@@ -171,13 +136,16 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
* <p>This method must only be called from the main thread.</p>
*/
void startSecondaryUserServicesIfNeeded() {
- startServicesIfNeeded(SERVICES_PER_USER);
+ String[] names =
+ getResources().getStringArray(R.array.config_systemUIServiceComponentsPerUser);
+ startServicesIfNeeded(names);
}
- private void startServicesIfNeeded(Class<?>[] services) {
+ private void startServicesIfNeeded(String[] services) {
if (mServicesStarted) {
return;
}
+ mServices = new SystemUI[services.length];
if (!mBootCompleted) {
// check to see if maybe it was already completed long before we began
@@ -195,14 +163,16 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
log.traceBegin("StartServices");
final int N = services.length;
for (int i = 0; i < N; i++) {
- Class<?> cl = services[i];
- if (DEBUG) Log.d(TAG, "loading: " + cl);
- log.traceBegin("StartServices" + cl.getSimpleName());
+ String clsName = services[i];
+ if (DEBUG) Log.d(TAG, "loading: " + clsName);
+ log.traceBegin("StartServices" + clsName);
long ti = System.currentTimeMillis();
+ Class cls;
try {
-
- Object newService = SystemUIFactory.getInstance().createInstance(cl);
- mServices[i] = (SystemUI) ((newService == null) ? cl.newInstance() : newService);
+ cls = Class.forName(clsName);
+ mServices[i] = (SystemUI) cls.newInstance();
+ } catch(ClassNotFoundException ex){
+ throw new RuntimeException(ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch (InstantiationException ex) {
@@ -218,7 +188,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv
// Warn if initialization of component takes too long
ti = System.currentTimeMillis() - ti;
if (ti > 1000) {
- Log.w(TAG, "Initialization of " + cl.getName() + " took " + ti + " ms");
+ Log.w(TAG, "Initialization of " + cls.getName() + " took " + ti + " ms");
}
if (mBootCompleted) {
mServices[i].onBootCompleted();
diff --git a/com/android/systemui/SystemUIFactory.java b/com/android/systemui/SystemUIFactory.java
index 526a8f46..0f3daf57 100644
--- a/com/android/systemui/SystemUIFactory.java
+++ b/com/android/systemui/SystemUIFactory.java
@@ -16,30 +16,42 @@
package com.android.systemui;
+import android.app.AlarmManager;
import android.content.Context;
import android.util.ArrayMap;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
+import com.android.internal.logging.MetricsLogger;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.Dependency.DependencyProvider;
import com.android.systemui.keyguard.DismissCallbackRegistry;
+import com.android.systemui.qs.QSTileHost;
import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.NotificationEntryManager;
+import com.android.systemui.statusbar.NotificationGutsManager;
+import com.android.systemui.statusbar.NotificationListener;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.NotificationLogger;
+import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationViewHierarchyManager;
import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
import com.android.systemui.statusbar.phone.LightBarController;
import com.android.systemui.statusbar.phone.LockIcon;
import com.android.systemui.statusbar.phone.LockscreenWallpaper;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.qs.QSTileHost;
import com.android.systemui.statusbar.phone.ScrimController;
+import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.volume.VolumeDialogControllerImpl;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import java.util.function.Consumer;
@@ -87,10 +99,10 @@ public class SystemUIFactory {
public ScrimController createScrimController(LightBarController lightBarController,
ScrimView scrimBehind, ScrimView scrimInFront, View headsUpScrim,
- LockscreenWallpaper lockscreenWallpaper, Consumer<Boolean> scrimVisibleListener,
- DozeParameters dozeParameters) {
+ LockscreenWallpaper lockscreenWallpaper, Consumer<Integer> scrimVisibleListener,
+ DozeParameters dozeParameters, AlarmManager alarmManager) {
return new ScrimController(lightBarController, scrimBehind, scrimInFront, headsUpScrim,
- scrimVisibleListener, dozeParameters);
+ scrimVisibleListener, dozeParameters, alarmManager);
}
public NotificationIconAreaController createNotificationIconAreaController(Context context,
@@ -108,10 +120,20 @@ public class SystemUIFactory {
return new QSTileHost(context, statusBar, iconController);
}
- public <T> T createInstance(Class<T> classType) {
- return null;
- }
-
public void injectDependencies(ArrayMap<Object, DependencyProvider> providers,
- Context context) { }
+ Context context) {
+ providers.put(NotificationLockscreenUserManager.class,
+ () -> new NotificationLockscreenUserManager(context));
+ providers.put(VisualStabilityManager.class, VisualStabilityManager::new);
+ providers.put(NotificationGroupManager.class, NotificationGroupManager::new);
+ providers.put(NotificationMediaManager.class, () -> new NotificationMediaManager(context));
+ providers.put(NotificationGutsManager.class, () -> new NotificationGutsManager(context));
+ providers.put(NotificationRemoteInputManager.class,
+ () -> new NotificationRemoteInputManager(context));
+ providers.put(NotificationListener.class, () -> new NotificationListener(context));
+ providers.put(NotificationLogger.class, NotificationLogger::new);
+ providers.put(NotificationViewHierarchyManager.class,
+ () -> new NotificationViewHierarchyManager(context));
+ providers.put(NotificationEntryManager.class, () -> new NotificationEntryManager(context));
+ }
}
diff --git a/com/android/systemui/car/CarNotificationEntryManager.java b/com/android/systemui/car/CarNotificationEntryManager.java
new file mode 100644
index 00000000..a89a8ef5
--- /dev/null
+++ b/com/android/systemui/car/CarNotificationEntryManager.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.android.systemui.car;
+
+import android.content.Context;
+import android.service.notification.StatusBarNotification;
+
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.NotificationEntryManager;
+
+public class CarNotificationEntryManager extends NotificationEntryManager {
+ public CarNotificationEntryManager(Context context) {
+ super(context);
+ }
+
+ /**
+ * Returns the
+ * {@link com.android.systemui.statusbar.ExpandableNotificationRow.LongPressListener} that will
+ * be triggered when a notification card is long-pressed.
+ */
+ @Override
+ public ExpandableNotificationRow.LongPressListener getNotificationLongClicker() {
+ // For the automative use case, we do not want to the user to be able to interact with
+ // a notification other than a regular click. As a result, just return null for the
+ // long click listener.
+ return null;
+ }
+
+ @Override
+ public boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn) {
+ // Because space is usually constrained in the auto use-case, there should not be a
+ // pinned notification when the shade has been expanded. Ensure this by not pinning any
+ // notification if the shade is already opened.
+ if (!mPresenter.isPresenterFullyCollapsed()) {
+ return false;
+ }
+
+ return super.shouldPeek(entry, sbn);
+ }
+}
diff --git a/com/android/systemui/car/CarSystemUIFactory.java b/com/android/systemui/car/CarSystemUIFactory.java
index 5a19e7dc..174584de 100644
--- a/com/android/systemui/car/CarSystemUIFactory.java
+++ b/com/android/systemui/car/CarSystemUIFactory.java
@@ -18,9 +18,22 @@ package com.android.systemui.car;
import android.content.Context;
import android.util.ArrayMap;
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.Dependency;
import com.android.systemui.Dependency.DependencyProvider;
+import com.android.systemui.ForegroundServiceController;
import com.android.systemui.SystemUIFactory;
+import com.android.systemui.UiOffloadThread;
import com.android.systemui.plugins.VolumeDialogController;
+import com.android.systemui.statusbar.NotificationEntryManager;
+import com.android.systemui.statusbar.NotificationGutsManager;
+import com.android.systemui.statusbar.NotificationListener;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.volume.car.CarVolumeDialogController;
/**
@@ -32,5 +45,7 @@ public class CarSystemUIFactory extends SystemUIFactory {
Context context) {
super.injectDependencies(providers, context);
providers.put(VolumeDialogController.class, () -> new CarVolumeDialogController(context));
+ providers.put(NotificationEntryManager.class,
+ () -> new CarNotificationEntryManager(context));
}
}
diff --git a/com/android/systemui/doze/AlwaysOnDisplayPolicy.java b/com/android/systemui/doze/AlwaysOnDisplayPolicy.java
index debda210..8515bf2e 100644
--- a/com/android/systemui/doze/AlwaysOnDisplayPolicy.java
+++ b/com/android/systemui/doze/AlwaysOnDisplayPolicy.java
@@ -30,8 +30,6 @@ import android.util.Log;
import com.android.systemui.R;
-import java.util.Arrays;
-
/**
* Class to store the policy for AOD, which comes from
* {@link android.provider.Settings.Global}
@@ -42,12 +40,16 @@ public class AlwaysOnDisplayPolicy {
private static final long DEFAULT_PROX_SCREEN_OFF_DELAY_MS = 10 * DateUtils.SECOND_IN_MILLIS;
private static final long DEFAULT_PROX_COOLDOWN_TRIGGER_MS = 2 * DateUtils.SECOND_IN_MILLIS;
private static final long DEFAULT_PROX_COOLDOWN_PERIOD_MS = 5 * DateUtils.SECOND_IN_MILLIS;
+ private static final long DEFAULT_WALLPAPER_VISIBILITY_MS = 60 * DateUtils.SECOND_IN_MILLIS;
+ private static final long DEFAULT_WALLPAPER_FADE_OUT_MS = 400;
static final String KEY_SCREEN_BRIGHTNESS_ARRAY = "screen_brightness_array";
static final String KEY_DIMMING_SCRIM_ARRAY = "dimming_scrim_array";
static final String KEY_PROX_SCREEN_OFF_DELAY_MS = "prox_screen_off_delay";
static final String KEY_PROX_COOLDOWN_TRIGGER_MS = "prox_cooldown_trigger";
static final String KEY_PROX_COOLDOWN_PERIOD_MS = "prox_cooldown_period";
+ static final String KEY_WALLPAPER_VISIBILITY_MS = "wallpaper_visibility_timeout";
+ static final String KEY_WALLPAPER_FADE_OUT_MS = "wallpaper_fade_out_duration";
/**
* Integer array to map ambient brightness type to real screen brightness.
@@ -91,6 +93,24 @@ public class AlwaysOnDisplayPolicy {
*/
public long proxCooldownPeriodMs;
+ /**
+ * For how long(ms) the wallpaper should still be visible
+ * after entering AoD.
+ *
+ * @see Settings.Global#ALWAYS_ON_DISPLAY_CONSTANTS
+ * @see #KEY_WALLPAPER_VISIBILITY_MS
+ */
+ public long wallpaperVisibilityDuration;
+
+ /**
+ * Duration(ms) of the fade out animation after
+ * {@link #KEY_WALLPAPER_VISIBILITY_MS} elapses.
+ *
+ * @see Settings.Global#ALWAYS_ON_DISPLAY_CONSTANTS
+ * @see #KEY_WALLPAPER_FADE_OUT_MS
+ */
+ public long wallpaperFadeOutDuration;
+
private final KeyValueListParser mParser;
private final Context mContext;
private SettingsObserver mSettingsObserver;
@@ -102,20 +122,6 @@ public class AlwaysOnDisplayPolicy {
mSettingsObserver.observe();
}
- private int[] parseIntArray(final String key, final int[] defaultArray) {
- final String value = mParser.getString(key, null);
- if (value != null) {
- try {
- return Arrays.stream(value.split(":")).map(String::trim).mapToInt(
- Integer::parseInt).toArray();
- } catch (NumberFormatException e) {
- return defaultArray;
- }
- } else {
- return defaultArray;
- }
- }
-
private final class SettingsObserver extends ContentObserver {
private final Uri ALWAYS_ON_DISPLAY_CONSTANTS_URI
= Settings.Global.getUriFor(Settings.Global.ALWAYS_ON_DISPLAY_CONSTANTS);
@@ -154,10 +160,14 @@ public class AlwaysOnDisplayPolicy {
DEFAULT_PROX_COOLDOWN_TRIGGER_MS);
proxCooldownPeriodMs = mParser.getLong(KEY_PROX_COOLDOWN_PERIOD_MS,
DEFAULT_PROX_COOLDOWN_PERIOD_MS);
- screenBrightnessArray = parseIntArray(KEY_SCREEN_BRIGHTNESS_ARRAY,
+ wallpaperFadeOutDuration = mParser.getLong(KEY_WALLPAPER_FADE_OUT_MS,
+ DEFAULT_WALLPAPER_FADE_OUT_MS);
+ wallpaperVisibilityDuration = mParser.getLong(KEY_WALLPAPER_VISIBILITY_MS,
+ DEFAULT_WALLPAPER_VISIBILITY_MS);
+ screenBrightnessArray = mParser.getIntArray(KEY_SCREEN_BRIGHTNESS_ARRAY,
resources.getIntArray(
R.array.config_doze_brightness_sensor_to_brightness));
- dimmingScrimArray = parseIntArray(KEY_DIMMING_SCRIM_ARRAY,
+ dimmingScrimArray = mParser.getIntArray(KEY_DIMMING_SCRIM_ARRAY,
resources.getIntArray(
R.array.config_doze_brightness_sensor_to_scrim_opacity));
}
diff --git a/com/android/systemui/doze/DozeFactory.java b/com/android/systemui/doze/DozeFactory.java
index 6f8bcff1..0f0402d0 100644
--- a/com/android/systemui/doze/DozeFactory.java
+++ b/com/android/systemui/doze/DozeFactory.java
@@ -63,9 +63,10 @@ public class DozeFactory {
new DozeFalsingManagerAdapter(FalsingManager.getInstance(context)),
createDozeTriggers(context, sensorManager, host, alarmManager, config, params,
handler, wakeLock, machine),
- createDozeUi(context, host, wakeLock, machine, handler, alarmManager),
+ createDozeUi(context, host, wakeLock, machine, handler, alarmManager, params),
new DozeScreenState(wrappedService, handler),
createDozeScreenBrightness(context, wrappedService, sensorManager, host, handler),
+ new DozeWallpaperState()
});
return machine;
@@ -89,8 +90,9 @@ public class DozeFactory {
}
private DozeMachine.Part createDozeUi(Context context, DozeHost host, WakeLock wakeLock,
- DozeMachine machine, Handler handler, AlarmManager alarmManager) {
- return new DozeUi(context, alarmManager, machine, wakeLock, host, handler);
+ DozeMachine machine, Handler handler, AlarmManager alarmManager,
+ DozeParameters params) {
+ return new DozeUi(context, alarmManager, machine, wakeLock, host, handler, params);
}
public static DozeHost getHost(DozeService service) {
diff --git a/com/android/systemui/doze/DozeHost.java b/com/android/systemui/doze/DozeHost.java
index 2f607eee..6a292998 100644
--- a/com/android/systemui/doze/DozeHost.java
+++ b/com/android/systemui/doze/DozeHost.java
@@ -38,6 +38,7 @@ public interface DozeHost {
void extendPulse();
void setAnimateWakeup(boolean animateWakeup);
+ void setAnimateScreenOff(boolean animateScreenOff);
void onDoubleTap(float x, float y);
diff --git a/com/android/systemui/doze/DozeService.java b/com/android/systemui/doze/DozeService.java
index 6650cc63..34d39286 100644
--- a/com/android/systemui/doze/DozeService.java
+++ b/com/android/systemui/doze/DozeService.java
@@ -24,9 +24,10 @@ import android.util.Log;
import com.android.systemui.Dependency;
import com.android.systemui.plugins.DozeServicePlugin;
-import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.DozeServicePlugin.RequestDoze;
import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginManager;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
diff --git a/com/android/systemui/doze/DozeUi.java b/com/android/systemui/doze/DozeUi.java
index 851b78cf..b352ec97 100644
--- a/com/android/systemui/doze/DozeUi.java
+++ b/com/android/systemui/doze/DozeUi.java
@@ -23,6 +23,7 @@ import android.os.SystemClock;
import android.text.format.Formatter;
import android.util.Log;
+import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.util.AlarmTimeout;
import com.android.systemui.util.wakelock.WakeLock;
@@ -41,18 +42,22 @@ public class DozeUi implements DozeMachine.Part {
private final WakeLock mWakeLock;
private final DozeMachine mMachine;
private final AlarmTimeout mTimeTicker;
+ private final boolean mCanAnimateWakeup;
private long mLastTimeTickElapsed = 0;
public DozeUi(Context context, AlarmManager alarmManager, DozeMachine machine,
- WakeLock wakeLock, DozeHost host, Handler handler) {
+ WakeLock wakeLock, DozeHost host, Handler handler,
+ DozeParameters params) {
mContext = context;
mMachine = machine;
mWakeLock = wakeLock;
mHost = host;
mHandler = handler;
+ mCanAnimateWakeup = !params.getDisplayNeedsBlanking();
mTimeTicker = new AlarmTimeout(alarmManager, this::onTimeTick, "doze_time_tick", handler);
+ mHost.setAnimateScreenOff(params.getCanControlScreenOffAnimation());
}
private void pulseWhileDozing(int reason) {
@@ -106,7 +111,7 @@ public class DozeUi implements DozeMachine.Part {
// Keep current state.
break;
default:
- mHost.setAnimateWakeup(false);
+ mHost.setAnimateWakeup(mCanAnimateWakeup);
break;
}
}
diff --git a/com/android/systemui/doze/DozeWallpaperState.java b/com/android/systemui/doze/DozeWallpaperState.java
new file mode 100644
index 00000000..ee41001d
--- /dev/null
+++ b/com/android/systemui/doze/DozeWallpaperState.java
@@ -0,0 +1,84 @@
+/*
+ * 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 com.android.systemui.doze;
+
+import android.app.IWallpaperManager;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+/**
+ * Propagates doze state to wallpaper engine.
+ */
+public class DozeWallpaperState implements DozeMachine.Part {
+
+ private static final String TAG = "DozeWallpaperState";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ @VisibleForTesting
+ final IWallpaperManager mWallpaperManagerService;
+ private boolean mIsAmbientMode;
+
+ public DozeWallpaperState() {
+ this(IWallpaperManager.Stub.asInterface(
+ ServiceManager.getService(Context.WALLPAPER_SERVICE)));
+ }
+
+ @VisibleForTesting
+ DozeWallpaperState(IWallpaperManager wallpaperManagerService) {
+ mWallpaperManagerService = wallpaperManagerService;
+ }
+
+ @Override
+ public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
+ final boolean isAmbientMode;
+ switch (newState) {
+ case DOZE_AOD:
+ case DOZE_AOD_PAUSING:
+ case DOZE_AOD_PAUSED:
+ case DOZE_REQUEST_PULSE:
+ case DOZE_PULSING:
+ case DOZE_PULSE_DONE:
+ isAmbientMode = true;
+ break;
+ default:
+ isAmbientMode = false;
+ }
+
+ if (isAmbientMode != mIsAmbientMode) {
+ mIsAmbientMode = isAmbientMode;
+ try {
+ Log.i(TAG, "AoD wallpaper state changed to: " + mIsAmbientMode);
+ mWallpaperManagerService.setInAmbientMode(mIsAmbientMode);
+ } catch (RemoteException e) {
+ // Cannot notify wallpaper manager service, but it's fine, let's just skip it.
+ Log.w(TAG, "Cannot notify state to WallpaperManagerService: " + mIsAmbientMode);
+ }
+ }
+ }
+
+ @Override
+ public void dump(PrintWriter pw) {
+ pw.println("DozeWallpaperState:");
+ pw.println(" isAmbientMode: " + mIsAmbientMode);
+ }
+}
diff --git a/com/android/systemui/globalactions/GlobalActionsDialog.java b/com/android/systemui/globalactions/GlobalActionsDialog.java
index 00e8b1a4..5c8c3f35 100644
--- a/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -19,6 +19,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR
import android.app.ActivityManager;
import android.app.Dialog;
import android.app.WallpaperManager;
+import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
@@ -112,11 +113,13 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
private static final String GLOBAL_ACTION_KEY_VOICEASSIST = "voiceassist";
private static final String GLOBAL_ACTION_KEY_ASSIST = "assist";
private static final String GLOBAL_ACTION_KEY_RESTART = "restart";
+ private static final String GLOBAL_ACTION_KEY_LOGOUT = "logout";
private final Context mContext;
private final GlobalActionsManager mWindowManagerFuncs;
private final AudioManager mAudioManager;
private final IDreamManager mDreamManager;
+ private final DevicePolicyManager mDevicePolicyManager;
private ArrayList<Action> mItems;
private ActionsDialog mDialog;
@@ -132,6 +135,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
private boolean mIsWaitingForEcmExit = false;
private boolean mHasTelephony;
private boolean mHasVibrator;
+ private boolean mHasLogoutButton;
private final boolean mShowSilentToggle;
private final EmergencyAffordanceManager mEmergencyAffordanceManager;
@@ -144,6 +148,8 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mDreamManager = IDreamManager.Stub.asInterface(
ServiceManager.getService(DreamService.DREAM_SERVICE));
+ mDevicePolicyManager = (DevicePolicyManager) mContext.getSystemService(
+ Context.DEVICE_POLICY_SERVICE);
// receive broadcasts
IntentFilter filter = new IntentFilter();
@@ -289,6 +295,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
R.array.config_globalActionsList);
ArraySet<String> addedKeys = new ArraySet<String>();
+ mHasLogoutButton = false;
for (int i = 0; i < defaultActions.length; i++) {
String actionKey = defaultActions[i];
if (addedKeys.contains(actionKey)) {
@@ -325,6 +332,12 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
mItems.add(getAssistAction());
} else if (GLOBAL_ACTION_KEY_RESTART.equals(actionKey)) {
mItems.add(new RestartAction());
+ } else if (GLOBAL_ACTION_KEY_LOGOUT.equals(actionKey)) {
+ if (mDevicePolicyManager.isLogoutEnabled()
+ && getCurrentUser().id != UserHandle.USER_SYSTEM) {
+ mItems.add(new LogoutAction());
+ mHasLogoutButton = true;
+ }
} else {
Log.e(TAG, "Invalid global action key " + actionKey);
}
@@ -490,6 +503,37 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
}
}
+ private final class LogoutAction extends SinglePressAction {
+ private LogoutAction() {
+ super(R.drawable.ic_logout, R.string.global_action_logout);
+ }
+
+ @Override
+ public boolean showDuringKeyguard() {
+ return true;
+ }
+
+ @Override
+ public boolean showBeforeProvisioning() {
+ return false;
+ }
+
+ @Override
+ public void onPress() {
+ // Add a little delay before executing, to give the dialog a chance to go away before
+ // switching user
+ mHandler.postDelayed(() -> {
+ try {
+ ActivityManager.getService().switchUser(UserHandle.USER_SYSTEM);
+ ActivityManager.getService().stopUser(getCurrentUser().id, true /*force*/,
+ null);
+ } catch (RemoteException re) {
+ Log.e(TAG, "Couldn't logout user " + re);
+ }
+ }, 500);
+ }
+ }
+
private Action getSettingsAction() {
return new SinglePressAction(R.drawable.ic_settings,
R.string.global_action_settings) {
@@ -764,7 +808,10 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
public View getView(int position, View convertView, ViewGroup parent) {
Action action = getItem(position);
View view = action.create(mContext, convertView, parent, LayoutInflater.from(mContext));
- if (position == 2) {
+ // When there is no logout button, only power off and restart should be in white
+ // background, thus setting division view at third item; with logout button being the
+ // third item, set the division view at fourth item instead.
+ if (position == (mHasLogoutButton ? 3 : 2)) {
HardwareUiLayout.get(parent).setDivisionView(view);
}
return view;
diff --git a/com/android/systemui/keyguard/KeyguardSliceProvider.java b/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 6ddc76b5..bd46c5f8 100644
--- a/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -16,38 +16,55 @@
package com.android.systemui.keyguard;
+import android.app.ActivityManager;
+import android.app.AlarmManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.graphics.drawable.Icon;
import android.icu.text.DateFormat;
import android.icu.text.DisplayContext;
import android.net.Uri;
import android.os.Handler;
-import android.app.slice.Slice;
-import android.app.slice.SliceProvider;
+import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.NextAlarmController;
+import com.android.systemui.statusbar.policy.NextAlarmControllerImpl;
import java.util.Date;
import java.util.Locale;
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceProvider;
+import androidx.app.slice.builders.ListBuilder;
+import androidx.app.slice.builders.ListBuilder.RowBuilder;
+
/**
* Simple Slice provider that shows the current date.
*/
-public class KeyguardSliceProvider extends SliceProvider {
+public class KeyguardSliceProvider extends SliceProvider implements
+ NextAlarmController.NextAlarmChangeCallback {
public static final String KEYGUARD_SLICE_URI = "content://com.android.systemui.keyguard/main";
+ public static final String KEYGUARD_DATE_URI = "content://com.android.systemui.keyguard/date";
+ public static final String KEYGUARD_NEXT_ALARM_URI =
+ "content://com.android.systemui.keyguard/alarm";
private final Date mCurrentTime = new Date();
protected final Uri mSliceUri;
+ protected final Uri mDateUri;
+ protected final Uri mAlarmUri;
private final Handler mHandler;
private String mDatePattern;
private DateFormat mDateFormat;
private String mLastText;
private boolean mRegistered;
private boolean mRegisteredEveryMinute;
+ private String mNextAlarm;
+ private NextAlarmController mNextAlarmController;
/**
* Receiver responsible for time ticking and updating the date format.
@@ -80,23 +97,49 @@ public class KeyguardSliceProvider extends SliceProvider {
KeyguardSliceProvider(Handler handler) {
mHandler = handler;
mSliceUri = Uri.parse(KEYGUARD_SLICE_URI);
+ mDateUri = Uri.parse(KEYGUARD_DATE_URI);
+ mAlarmUri = Uri.parse(KEYGUARD_NEXT_ALARM_URI);
}
@Override
public Slice onBindSlice(Uri sliceUri) {
- return new Slice.Builder(sliceUri).addText(mLastText, null, Slice.HINT_TITLE).build();
+ ListBuilder builder = new ListBuilder(mSliceUri)
+ .addRow(new RowBuilder(mDateUri).setTitle(mLastText));
+ if (!TextUtils.isEmpty(mNextAlarm)) {
+ Icon icon = Icon.createWithResource(getContext(), R.drawable.ic_access_alarms_big);
+ builder.addRow(new RowBuilder(mAlarmUri).setTitle(mNextAlarm).addEndItem(icon));
+ }
+
+ return builder.build();
}
@Override
- public boolean onCreate() {
-
+ public boolean onCreateSliceProvider() {
+ mNextAlarmController = new NextAlarmControllerImpl(getContext());
+ mNextAlarmController.addCallback(this);
mDatePattern = getContext().getString(R.string.system_ui_date_pattern);
-
registerClockUpdate(false /* everyMinute */);
updateClock();
return true;
}
+ public static String formatNextAlarm(Context context, AlarmManager.AlarmClockInfo info) {
+ if (info == null) {
+ return "";
+ }
+ String skeleton = android.text.format.DateFormat
+ .is24HourFormat(context, ActivityManager.getCurrentUser()) ? "EHm" : "Ehma";
+ String pattern = android.text.format.DateFormat
+ .getBestDateTimePattern(Locale.getDefault(), skeleton);
+ return android.text.format.DateFormat.format(pattern, info.getTriggerTime()).toString();
+ }
+
+ /**
+ * Registers a broadcast receiver for clock updates, include date, time zone and manually
+ * changing the date/time via the settings app.
+ *
+ * @param everyMinute {@code true} if you also want updates every minute.
+ */
protected void registerClockUpdate(boolean everyMinute) {
if (mRegistered) {
if (mRegisteredEveryMinute == everyMinute) {
@@ -156,4 +199,10 @@ public class KeyguardSliceProvider extends SliceProvider {
void cleanDateFormat() {
mDateFormat = null;
}
+
+ @Override
+ public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
+ mNextAlarm = formatNextAlarm(getContext(), nextAlarm);
+ getContext().getContentResolver().notifyChange(mSliceUri, null /* observer */);
+ }
}
diff --git a/com/android/systemui/keyguard/KeyguardViewMediator.java b/com/android/systemui/keyguard/KeyguardViewMediator.java
index c92acd06..91ae4485 100644
--- a/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -25,7 +25,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT;
-import android.app.Activity;
+
import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.NotificationManager;
@@ -53,7 +53,6 @@ import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
-import android.provider.Settings.System;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.EventLog;
@@ -77,20 +76,16 @@ import com.android.keyguard.KeyguardDisplayManager;
import com.android.keyguard.KeyguardSecurityView;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.keyguard.LatencyTracker;
+import com.android.internal.util.LatencyTracker;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.Dependency;
import com.android.systemui.SystemUI;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.classifier.FalsingManager;
-import com.android.systemui.recents.Recents;
-import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.statusbar.phone.FingerprintUnlockController;
import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.StatusBarWindowManager;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -169,7 +164,6 @@ public class KeyguardViewMediator extends SystemUI {
private static final int NOTIFY_SCREEN_TURNED_ON = 15;
private static final int NOTIFY_SCREEN_TURNED_OFF = 16;
private static final int NOTIFY_STARTED_GOING_TO_SLEEP = 17;
- private static final int SET_SWITCHING_USER = 18;
/**
* The default amount of time we stay awake (used for all key input)
@@ -619,6 +613,13 @@ public class KeyguardViewMediator extends SystemUI {
}
@Override
+ public void onBouncerVisiblityChanged(boolean shown) {
+ synchronized (KeyguardViewMediator.this) {
+ adjustStatusBarLocked(shown);
+ }
+ }
+
+ @Override
public void playTrustedSound() {
KeyguardViewMediator.this.playTrustedSound();
}
@@ -871,7 +872,7 @@ public class KeyguardViewMediator extends SystemUI {
// From DevicePolicyAdmin
final long policyTimeout = mLockPatternUtils.getDevicePolicyManager()
- .getMaximumTimeToLockForUserAndProfiles(userId);
+ .getMaximumTimeToLock(null, userId);
long timeout;
@@ -1424,11 +1425,7 @@ public class KeyguardViewMediator extends SystemUI {
}
public void setSwitchingUser(boolean switching) {
- Trace.beginSection("KeyguardViewMediator#setSwitchingUser");
- mHandler.removeMessages(SET_SWITCHING_USER);
- Message msg = mHandler.obtainMessage(SET_SWITCHING_USER, switching ? 1 : 0, 0);
- mHandler.sendMessage(msg);
- Trace.endSection();
+ KeyguardUpdateMonitor.getInstance(mContext).setSwitchingUser(switching);
}
/**
@@ -1568,11 +1565,6 @@ public class KeyguardViewMediator extends SystemUI {
Log.w(TAG, "Timeout while waiting for activity drawn!");
Trace.endSection();
break;
- case SET_SWITCHING_USER:
- Trace.beginSection("KeyguardViewMediator#handleMessage SET_SWITCHING_USER");
- KeyguardUpdateMonitor.getInstance(mContext).setSwitchingUser(msg.arg1 != 0);
- Trace.endSection();
- break;
}
}
};
@@ -1877,6 +1869,10 @@ public class KeyguardViewMediator extends SystemUI {
}
private void adjustStatusBarLocked() {
+ adjustStatusBarLocked(false /* forceHideHomeRecentsButtons */);
+ }
+
+ private void adjustStatusBarLocked(boolean forceHideHomeRecentsButtons) {
if (mStatusBarManager == null) {
mStatusBarManager = (StatusBarManager)
mContext.getSystemService(Context.STATUS_BAR_SERVICE);
@@ -1887,19 +1883,14 @@ public class KeyguardViewMediator extends SystemUI {
// Disable aspects of the system/status/navigation bars that must not be re-enabled by
// windows that appear on top, ever
int flags = StatusBarManager.DISABLE_NONE;
- if (mShowing) {
- // Permanently disable components not available when keyguard is enabled
- // (like recents). Temporary enable/disable (e.g. the "back" button) are
- // done in KeyguardHostView.
- flags |= StatusBarManager.DISABLE_RECENT;
- }
- if (isShowingAndNotOccluded()) {
- flags |= StatusBarManager.DISABLE_HOME;
+ if (forceHideHomeRecentsButtons || isShowingAndNotOccluded()) {
+ flags |= StatusBarManager.DISABLE_HOME | StatusBarManager.DISABLE_RECENT;
}
if (DEBUG) {
Log.d(TAG, "adjustStatusBarLocked: mShowing=" + mShowing + " mOccluded=" + mOccluded
- + " isSecure=" + isSecure() + " --> flags=0x" + Integer.toHexString(flags));
+ + " isSecure=" + isSecure() + " force=" + forceHideHomeRecentsButtons
+ + " --> flags=0x" + Integer.toHexString(flags));
}
mStatusBarManager.disable(flags);
diff --git a/com/android/systemui/media/NotificationPlayer.java b/com/android/systemui/media/NotificationPlayer.java
index b5c0d538..f5f06db3 100644
--- a/com/android/systemui/media/NotificationPlayer.java
+++ b/com/android/systemui/media/NotificationPlayer.java
@@ -133,12 +133,19 @@ public class NotificationPlayer implements OnCompletionListener, OnErrorListener
+ mNotificationRampTimeMs + "ms"); }
try {
Thread.sleep(mNotificationRampTimeMs);
- player.start();
} catch (InterruptedException e) {
Log.e(mTag, "Exception while sleeping to sync notification playback"
+ " with ducking", e);
}
- if (DEBUG) { Log.d(mTag, "player.start"); }
+ try {
+ player.start();
+ if (DEBUG) { Log.d(mTag, "player.start"); }
+ } catch (Exception e) {
+ player.release();
+ player = null;
+ // playing the notification didn't work, revert the focus request
+ abandonAudioFocusAfterError();
+ }
if (mPlayer != null) {
if (DEBUG) { Log.d(mTag, "mPlayer.release"); }
mPlayer.release();
@@ -147,6 +154,8 @@ public class NotificationPlayer implements OnCompletionListener, OnErrorListener
}
catch (Exception e) {
Log.w(mTag, "error loading sound for " + mCmd.uri, e);
+ // playing the notification didn't work, revert the focus request
+ abandonAudioFocusAfterError();
}
this.notify();
}
@@ -154,6 +163,16 @@ public class NotificationPlayer implements OnCompletionListener, OnErrorListener
}
};
+ private void abandonAudioFocusAfterError() {
+ synchronized (mQueueAudioFocusLock) {
+ if (mAudioManagerWithAudioFocus != null) {
+ if (DEBUG) Log.d(mTag, "abandoning focus after playback error");
+ mAudioManagerWithAudioFocus.abandonAudioFocus(null);
+ mAudioManagerWithAudioFocus = null;
+ }
+ }
+ }
+
private void startSound(Command cmd) {
// Preparing can be slow, so if there is something else
// is playing, let it continue until we're done, so there
diff --git a/com/android/systemui/pip/phone/PipManager.java b/com/android/systemui/pip/phone/PipManager.java
index 29635068..dce3e243 100644
--- a/com/android/systemui/pip/phone/PipManager.java
+++ b/com/android/systemui/pip/phone/PipManager.java
@@ -64,7 +64,6 @@ public class PipManager implements BasePipManager {
private InputConsumerController mInputConsumerController;
private PipMenuActivityController mMenuController;
private PipMediaController mMediaController;
- private PipNotificationController mNotificationController;
private PipTouchHandler mTouchHandler;
/**
@@ -76,8 +75,6 @@ public class PipManager implements BasePipManager {
mTouchHandler.onActivityPinned();
mMediaController.onActivityPinned();
mMenuController.onActivityPinned();
- mNotificationController.onActivityPinned(packageName, userId,
- true /* deferUntilAnimationEnds */);
SystemServicesProxy.getInstance(mContext).setPipVisibility(true);
}
@@ -90,7 +87,6 @@ public class PipManager implements BasePipManager {
final int userId = topActivity != null ? topPipActivityInfo.second : 0;
mMenuController.onActivityUnpinned();
mTouchHandler.onActivityUnpinned(topActivity);
- mNotificationController.onActivityUnpinned(topActivity, userId);
SystemServicesProxy.getInstance(mContext).setPipVisibility(topActivity != null);
}
@@ -107,7 +103,6 @@ public class PipManager implements BasePipManager {
mTouchHandler.setTouchEnabled(true);
mTouchHandler.onPinnedStackAnimationEnded();
mMenuController.onPinnedStackAnimationEnded();
- mNotificationController.onPinnedStackAnimationEnded();
}
@Override
@@ -182,8 +177,6 @@ public class PipManager implements BasePipManager {
mInputConsumerController);
mTouchHandler = new PipTouchHandler(context, mActivityManager, mMenuController,
mInputConsumerController);
- mNotificationController = new PipNotificationController(context, mActivityManager,
- mTouchHandler.getMotionHelper());
EventBus.getDefault().register(this);
}
@@ -198,20 +191,6 @@ public class PipManager implements BasePipManager {
* Expands the PIP.
*/
public final void onBusEvent(ExpandPipEvent event) {
- if (event.clearThumbnailWindows) {
- try {
- StackInfo stackInfo = mActivityManager.getStackInfo(
- WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
- if (stackInfo != null && stackInfo.taskIds != null) {
- ActivityManagerWrapper am = ActivityManagerWrapper.getInstance();
- for (int taskId : stackInfo.taskIds) {
- am.cancelThumbnailTransition(taskId);
- }
- }
- } catch (RemoteException e) {
- // Do nothing
- }
- }
mTouchHandler.getMotionHelper().expandPip(false /* skipAnimation */);
}
diff --git a/com/android/systemui/pip/phone/PipMenuActivity.java b/com/android/systemui/pip/phone/PipMenuActivity.java
index 90f7b8db..bfe07a98 100644
--- a/com/android/systemui/pip/phone/PipMenuActivity.java
+++ b/com/android/systemui/pip/phone/PipMenuActivity.java
@@ -16,6 +16,10 @@
package com.android.systemui.pip.phone;
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS;
+
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_ACTIONS;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_ALLOW_TIMEOUT;
import static com.android.systemui.pip.phone.PipMenuActivityController.EXTRA_CONTROLLER_MESSENGER;
@@ -39,6 +43,7 @@ import android.app.Activity;
import android.app.ActivityManager;
import android.app.PendingIntent.CanceledException;
import android.app.RemoteAction;
+import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.graphics.Color;
@@ -46,12 +51,15 @@ import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.util.Log;
+import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -105,6 +113,7 @@ public class PipMenuActivity extends Activity {
private Drawable mBackgroundDrawable;
private View mMenuContainer;
private LinearLayout mActionsGroup;
+ private View mSettingsButton;
private View mDismissButton;
private ImageView mExpandButton;
private int mBetweenActionPaddingLand;
@@ -218,6 +227,11 @@ public class PipMenuActivity extends Activity {
}
return true;
});
+ mSettingsButton = findViewById(R.id.settings);
+ mSettingsButton.setAlpha(0);
+ mSettingsButton.setOnClickListener((v) -> {
+ showSettings();
+ });
mDismissButton = findViewById(R.id.dismiss);
mDismissButton.setAlpha(0);
mDismissButton.setOnClickListener((v) -> {
@@ -352,12 +366,14 @@ public class PipMenuActivity extends Activity {
ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
mMenuContainer.getAlpha(), 1f);
menuAnim.addUpdateListener(mMenuBgUpdateListener);
+ ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+ mSettingsButton.getAlpha(), 1f);
ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
mDismissButton.getAlpha(), 1f);
if (menuState == MENU_STATE_FULL) {
- mMenuContainerAnimator.playTogether(menuAnim, dismissAnim);
+ mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim);
} else {
- mMenuContainerAnimator.play(dismissAnim);
+ mMenuContainerAnimator.playTogether(settingsAnim, dismissAnim);
}
mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
mMenuContainerAnimator.setDuration(MENU_FADE_DURATION);
@@ -394,9 +410,11 @@ public class PipMenuActivity extends Activity {
ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA,
mMenuContainer.getAlpha(), 0f);
menuAnim.addUpdateListener(mMenuBgUpdateListener);
+ ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA,
+ mSettingsButton.getAlpha(), 0f);
ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA,
mDismissButton.getAlpha(), 0f);
- mMenuContainerAnimator.playTogether(menuAnim, dismissAnim);
+ mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim);
mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT);
mMenuContainerAnimator.setDuration(MENU_FADE_DURATION);
mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() {
@@ -526,12 +544,14 @@ public class PipMenuActivity extends Activity {
final float menuAlpha = 1 - fraction;
if (mMenuState == MENU_STATE_FULL) {
mMenuContainer.setAlpha(menuAlpha);
+ mSettingsButton.setAlpha(menuAlpha);
mDismissButton.setAlpha(menuAlpha);
final float interpolatedAlpha =
MENU_BACKGROUND_ALPHA * menuAlpha + DISMISS_BACKGROUND_ALPHA * fraction;
alpha = (int) (interpolatedAlpha * 255);
} else {
if (mMenuState == MENU_STATE_CLOSE) {
+ mSettingsButton.setAlpha(menuAlpha);
mDismissButton.setAlpha(menuAlpha);
}
alpha = (int) (fraction * DISMISS_BACKGROUND_ALPHA * 255);
@@ -588,6 +608,19 @@ public class PipMenuActivity extends Activity {
sendMessage(m, "Could not notify controller to show PIP menu");
}
+ private void showSettings() {
+ final Pair<ComponentName, Integer> topPipActivityInfo =
+ PipUtils.getTopPinnedActivity(this, ActivityManager.getService());
+ if (topPipActivityInfo.first != null) {
+ final UserHandle user = UserHandle.of(topPipActivityInfo.second);
+ final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS,
+ Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null));
+ settingsIntent.putExtra(Intent.EXTRA_USER_HANDLE, user);
+ settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(settingsIntent);
+ }
+ }
+
private void notifyActivityCallback(Messenger callback) {
Message m = Message.obtain();
m.what = PipMenuActivityController.MESSAGE_UPDATE_ACTIVITY_CALLBACK;
diff --git a/com/android/systemui/pip/phone/PipNotificationController.java b/com/android/systemui/pip/phone/PipNotificationController.java
deleted file mode 100644
index 6d083e9d..00000000
--- a/com/android/systemui/pip/phone/PipNotificationController.java
+++ /dev/null
@@ -1,231 +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 com.android.systemui.pip.phone;
-
-import static android.app.AppOpsManager.MODE_ALLOWED;
-import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE;
-import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
-import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS;
-
-import android.app.AppOpsManager;
-import android.app.AppOpsManager.OnOpChangedListener;
-import android.app.IActivityManager;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.Canvas;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.UserHandle;
-import android.util.IconDrawableFactory;
-import android.util.Log;
-import android.util.Pair;
-
-import com.android.systemui.R;
-import com.android.systemui.SystemUI;
-import com.android.systemui.util.NotificationChannels;
-
-/**
- * Manages the BTW notification that shows whenever an activity enters or leaves picture-in-picture.
- */
-public class PipNotificationController {
- private static final String TAG = PipNotificationController.class.getSimpleName();
-
- private static final String NOTIFICATION_TAG = PipNotificationController.class.getName();
- private static final int NOTIFICATION_ID = 0;
-
- private Context mContext;
- private IActivityManager mActivityManager;
- private AppOpsManager mAppOpsManager;
- private NotificationManager mNotificationManager;
- private IconDrawableFactory mIconDrawableFactory;
-
- private PipMotionHelper mMotionHelper;
-
- // Used when building a deferred notification
- private String mDeferredNotificationPackageName;
- private int mDeferredNotificationUserId;
-
- private AppOpsManager.OnOpChangedListener mAppOpsChangedListener = new OnOpChangedListener() {
- @Override
- public void onOpChanged(String op, String packageName) {
- try {
- // Dismiss the PiP once the user disables the app ops setting for that package
- final Pair<ComponentName, Integer> topPipActivityInfo =
- PipUtils.getTopPinnedActivity(mContext, mActivityManager);
- if (topPipActivityInfo.first != null) {
- final ApplicationInfo appInfo = mContext.getPackageManager()
- .getApplicationInfoAsUser(packageName, 0, topPipActivityInfo.second);
- if (appInfo.packageName.equals(topPipActivityInfo.first.getPackageName()) &&
- mAppOpsManager.checkOpNoThrow(OP_PICTURE_IN_PICTURE, appInfo.uid,
- packageName) != MODE_ALLOWED) {
- mMotionHelper.dismissPip();
- }
- }
- } catch (NameNotFoundException e) {
- // Unregister the listener if the package can't be found
- unregisterAppOpsListener();
- }
- }
- };
-
- public PipNotificationController(Context context, IActivityManager activityManager,
- PipMotionHelper motionHelper) {
- mContext = context;
- mActivityManager = activityManager;
- mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
- mNotificationManager = NotificationManager.from(context);
- mMotionHelper = motionHelper;
- mIconDrawableFactory = IconDrawableFactory.newInstance(context);
- }
-
- public void onActivityPinned(String packageName, int userId, boolean deferUntilAnimationEnds) {
- // Clear any existing notification
- mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
-
- if (deferUntilAnimationEnds) {
- mDeferredNotificationPackageName = packageName;
- mDeferredNotificationUserId = userId;
- } else {
- showNotificationForApp(packageName, userId);
- }
-
- // Register for changes to the app ops setting for this package while it is in PiP
- registerAppOpsListener(packageName);
- }
-
- public void onPinnedStackAnimationEnded() {
- if (mDeferredNotificationPackageName != null) {
- showNotificationForApp(mDeferredNotificationPackageName, mDeferredNotificationUserId);
- mDeferredNotificationPackageName = null;
- mDeferredNotificationUserId = 0;
- }
- }
-
- public void onActivityUnpinned(ComponentName topPipActivity, int userId) {
- // Unregister for changes to the previously PiP'ed package
- unregisterAppOpsListener();
-
- // Reset the deferred notification package
- mDeferredNotificationPackageName = null;
- mDeferredNotificationUserId = 0;
-
- if (topPipActivity != null) {
- // onActivityUnpinned() is only called after the transition is complete, so we don't
- // need to defer until the animation ends to update the notification
- onActivityPinned(topPipActivity.getPackageName(), userId,
- false /* deferUntilAnimationEnds */);
- } else {
- mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
- }
- }
-
- /**
- * Builds and shows the notification for the given app.
- */
- private void showNotificationForApp(String packageName, int userId) {
- // Build a new notification
- try {
- final UserHandle user = UserHandle.of(userId);
- final Context userContext = mContext.createPackageContextAsUser(
- mContext.getPackageName(), 0, user);
- final Notification.Builder builder =
- new Notification.Builder(userContext, NotificationChannels.GENERAL)
- .setLocalOnly(true)
- .setOngoing(true)
- .setSmallIcon(R.drawable.pip_notification_icon)
- .setColor(mContext.getColor(
- com.android.internal.R.color.system_notification_accent_color));
- if (updateNotificationForApp(builder, packageName, user)) {
- SystemUI.overrideNotificationAppName(mContext, builder);
-
- // Show the new notification
- mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build());
- }
- } catch (NameNotFoundException e) {
- Log.e(TAG, "Could not show notification for application", e);
- }
- }
-
- /**
- * Updates the notification builder with app-specific information, returning whether it was
- * successful.
- */
- private boolean updateNotificationForApp(Notification.Builder builder, String packageName,
- UserHandle user) throws NameNotFoundException {
- final PackageManager pm = mContext.getPackageManager();
- final ApplicationInfo appInfo;
- try {
- appInfo = pm.getApplicationInfoAsUser(packageName, 0, user.getIdentifier());
- } catch (NameNotFoundException e) {
- Log.e(TAG, "Could not update notification for application", e);
- return false;
- }
-
- if (appInfo != null) {
- final String appName = pm.getUserBadgedLabel(pm.getApplicationLabel(appInfo), user)
- .toString();
- final String message = mContext.getString(R.string.pip_notification_message, appName);
- final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS,
- Uri.fromParts("package", packageName, null));
- settingsIntent.putExtra(Intent.EXTRA_USER_HANDLE, user);
- settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);
-
- final Drawable iconDrawable = mIconDrawableFactory.getBadgedIcon(appInfo);
- builder.setContentTitle(mContext.getString(R.string.pip_notification_title, appName))
- .setContentText(message)
- .setContentIntent(PendingIntent.getActivityAsUser(mContext, packageName.hashCode(),
- settingsIntent, FLAG_CANCEL_CURRENT, null, user))
- .setStyle(new Notification.BigTextStyle().bigText(message))
- .setLargeIcon(createBitmap(iconDrawable).createAshmemBitmap());
- return true;
- }
- return false;
- }
-
- private void registerAppOpsListener(String packageName) {
- mAppOpsManager.startWatchingMode(OP_PICTURE_IN_PICTURE, packageName,
- mAppOpsChangedListener);
- }
-
- private void unregisterAppOpsListener() {
- mAppOpsManager.stopWatchingMode(mAppOpsChangedListener);
- }
-
- /**
- * Bakes a drawable into a bitmap.
- */
- private Bitmap createBitmap(Drawable d) {
- Bitmap bitmap = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(),
- Config.ARGB_8888);
- Canvas c = new Canvas(bitmap);
- d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
- d.draw(c);
- c.setBitmap(null);
- return bitmap;
- }
-}
diff --git a/com/android/systemui/pip/phone/PipTouchHandler.java b/com/android/systemui/pip/phone/PipTouchHandler.java
index 51175d1d..2b48e0fb 100644
--- a/com/android/systemui/pip/phone/PipTouchHandler.java
+++ b/com/android/systemui/pip/phone/PipTouchHandler.java
@@ -387,9 +387,7 @@ public class PipTouchHandler {
}
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_MOVE: {
- if (mAccessibilityManager.isObservedEventType(
- AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
- && !mSendingHoverAccessibilityEvents) {
+ if (mAccessibilityManager.isEnabled() && !mSendingHoverAccessibilityEvents) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
event.setImportantForAccessibility(true);
@@ -402,9 +400,7 @@ public class PipTouchHandler {
break;
}
case MotionEvent.ACTION_HOVER_EXIT: {
- if (mAccessibilityManager.isObservedEventType(
- AccessibilityEvent.TYPE_VIEW_HOVER_EXIT)
- && mSendingHoverAccessibilityEvents) {
+ if (mAccessibilityManager.isEnabled() && mSendingHoverAccessibilityEvents) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
event.setImportantForAccessibility(true);
diff --git a/com/android/systemui/qs/QSFooterImpl.java b/com/android/systemui/qs/QSFooterImpl.java
index 5ffd7859..927a49cb 100644
--- a/com/android/systemui/qs/QSFooterImpl.java
+++ b/com/android/systemui/qs/QSFooterImpl.java
@@ -18,11 +18,6 @@ package com.android.systemui.qs;
import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
-import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_DATE;
-
-import android.app.ActivityManager;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
@@ -31,7 +26,6 @@ import android.graphics.PorterDuff.Mode;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.UserManager;
-import android.provider.AlarmClock;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.AttributeSet;
@@ -39,23 +33,19 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.widget.FrameLayout;
import android.widget.ImageView;
-import android.widget.TextView;
import android.widget.Toast;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
-import com.android.keyguard.KeyguardStatusView;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.settingslib.Utils;
import com.android.settingslib.drawable.UserIconDrawable;
import com.android.systemui.Dependency;
-import com.android.systemui.FontSizeUtils;
import com.android.systemui.R;
import com.android.systemui.R.dimen;
-import com.android.systemui.R.id;
import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.TouchAnimator.Builder;
-import com.android.systemui.qs.TouchAnimator.ListenerAdapter;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.ExpandableIndicator;
import com.android.systemui.statusbar.phone.MultiUserSwitch;
@@ -64,37 +54,27 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.NetworkController.EmergencyListener;
import com.android.systemui.statusbar.policy.NetworkController.SignalCallback;
-import com.android.systemui.statusbar.policy.NextAlarmController;
-import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback;
import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener;
import com.android.systemui.tuner.TunerService;
public class QSFooterImpl extends FrameLayout implements QSFooter,
- NextAlarmChangeCallback, OnClickListener, OnUserInfoChangedListener, EmergencyListener,
+ OnClickListener, OnUserInfoChangedListener, EmergencyListener,
SignalCallback, CommandQueue.Callbacks {
private static final float EXPAND_INDICATOR_THRESHOLD = .93f;
private ActivityStarter mActivityStarter;
- private NextAlarmController mNextAlarmController;
private UserInfoController mUserInfoController;
private SettingsButton mSettingsButton;
protected View mSettingsContainer;
- private TextView mAlarmStatus;
- private View mAlarmStatusCollapsed;
- private View mDate;
-
private boolean mQsDisabled;
private QSPanel mQsPanel;
private boolean mExpanded;
- private boolean mAlarmShowing;
-
protected ExpandableIndicator mExpandIndicator;
private boolean mListening;
- private AlarmManager.AlarmClockInfo mNextAlarm;
private boolean mShowEmergencyCallsOnly;
protected MultiUserSwitch mMultiUserSwitch;
@@ -105,9 +85,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
protected View mEdit;
private TouchAnimator mAnimator;
- private View mDateTimeGroup;
- private boolean mKeyguardShowing;
- private TouchAnimator mAlarmAnimator;
public QSFooterImpl(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -123,18 +100,11 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() ->
mQsPanel.showEdit(view)));
- mDateTimeGroup = findViewById(id.date_time_alarm_group);
- mDate = findViewById(R.id.date);
-
mExpandIndicator = findViewById(R.id.expand_indicator);
mSettingsButton = findViewById(R.id.settings_button);
mSettingsContainer = findViewById(R.id.settings_button_container);
mSettingsButton.setOnClickListener(this);
- mAlarmStatusCollapsed = findViewById(R.id.alarm_status_collapsed);
- mAlarmStatus = findViewById(R.id.alarm_status);
- mDateTimeGroup.setOnClickListener(this);
-
mMultiUserSwitch = findViewById(R.id.multi_user_switch);
mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar);
@@ -145,7 +115,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
updateResources();
- mNextAlarmController = Dependency.get(NextAlarmController.class);
mUserInfoController = Dependency.get(UserInfoController.class);
mActivityStarter = Dependency.get(ActivityStarter.class);
addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight,
@@ -164,28 +133,7 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
isLayoutRtl() ? (remaining - defSpace) : -(remaining - defSpace), 0)
.addFloat(mSettingsButton, "rotation", -120, 0)
.build();
- if (mAlarmShowing) {
- int translate = isLayoutRtl() ? mDate.getWidth() : -mDate.getWidth();
- mAlarmAnimator = new Builder().addFloat(mDate, "alpha", 1, 0)
- .addFloat(mDateTimeGroup, "translationX", 0, translate)
- .addFloat(mAlarmStatus, "alpha", 0, 1)
- .setListener(new ListenerAdapter() {
- @Override
- public void onAnimationAtStart() {
- mAlarmStatus.setVisibility(View.GONE);
- }
-
- @Override
- public void onAnimationStarted() {
- mAlarmStatus.setVisibility(View.VISIBLE);
- }
- }).build();
- } else {
- mAlarmAnimator = null;
- mAlarmStatus.setVisibility(View.GONE);
- mDate.setAlpha(1);
- mDateTimeGroup.setTranslationX(0);
- }
+
setExpansion(mExpansionAmount);
}
@@ -202,27 +150,11 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
}
private void updateResources() {
- FontSizeUtils.updateFontSize(mAlarmStatus, R.dimen.qs_date_collapsed_size);
-
updateSettingsAnimator();
}
private void updateSettingsAnimator() {
mSettingsAlpha = createSettingsAlphaAnimator();
-
- final boolean isRtl = isLayoutRtl();
- if (isRtl && mDate.getWidth() == 0) {
- mDate.addOnLayoutChangeListener(new OnLayoutChangeListener() {
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
- mDate.setPivotX(getWidth());
- mDate.removeOnLayoutChangeListener(this);
- }
- });
- } else {
- mDate.setPivotX(isRtl ? mDate.getWidth() : 0);
- }
}
@Nullable
@@ -235,7 +167,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
@Override
public void setKeyguardShowing(boolean keyguardShowing) {
- mKeyguardShowing = keyguardShowing;
setExpansion(mExpansionAmount);
}
@@ -247,36 +178,14 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
}
@Override
- public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
- mNextAlarm = nextAlarm;
- if (nextAlarm != null) {
- String alarmString = KeyguardStatusView.formatNextAlarm(getContext(), nextAlarm);
- mAlarmStatus.setText(alarmString);
- mAlarmStatus.setContentDescription(mContext.getString(
- R.string.accessibility_quick_settings_alarm, alarmString));
- mAlarmStatusCollapsed.setContentDescription(mContext.getString(
- R.string.accessibility_quick_settings_alarm, alarmString));
- }
- if (mAlarmShowing != (nextAlarm != null)) {
- mAlarmShowing = nextAlarm != null;
- updateAnimator(getWidth());
- updateEverything();
- }
- }
-
- @Override
public void setExpansion(float headerExpansionFraction) {
mExpansionAmount = headerExpansionFraction;
if (mAnimator != null) mAnimator.setPosition(headerExpansionFraction);
- if (mAlarmAnimator != null) mAlarmAnimator.setPosition(
- mKeyguardShowing ? 0 : headerExpansionFraction);
if (mSettingsAlpha != null) {
mSettingsAlpha.setPosition(headerExpansionFraction);
}
- updateAlarmVisibilities();
-
mExpandIndicator.setExpanded(headerExpansionFraction > EXPAND_INDICATOR_THRESHOLD);
}
@@ -294,10 +203,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
super.onDetachedFromWindow();
}
- private void updateAlarmVisibilities() {
- mAlarmStatusCollapsed.setVisibility(mAlarmShowing ? View.VISIBLE : View.GONE);
- }
-
@Override
public void setListening(boolean listening) {
if (listening == mListening) {
@@ -328,8 +233,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
}
private void updateVisibilities() {
- updateAlarmVisibilities();
-
mSettingsContainer.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility(
TunerService.isTunerEnabled(mContext) ? View.VISIBLE : View.INVISIBLE);
@@ -348,14 +251,12 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
private void updateListeners() {
if (mListening) {
- mNextAlarmController.addCallback(this);
mUserInfoController.addCallback(this);
if (Dependency.get(NetworkController.class).hasVoiceCallingFeature()) {
Dependency.get(NetworkController.class).addEmergencyListener(this);
Dependency.get(NetworkController.class).addCallback(this);
}
} else {
- mNextAlarmController.removeCallback(this);
mUserInfoController.removeCallback(this);
Dependency.get(NetworkController.class).removeEmergencyListener(this);
Dependency.get(NetworkController.class).removeCallback(this);
@@ -399,16 +300,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
} else {
startSettingsActivity();
}
- } else if (v == mDateTimeGroup) {
- Dependency.get(MetricsLogger.class).action(ACTION_QS_DATE,
- mNextAlarm != null);
- if (mNextAlarm != null) {
- PendingIntent showIntent = mNextAlarm.getShowIntent();
- mActivityStarter.startPendingIntentDismissingKeyguard(showIntent);
- } else {
- mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
- AlarmClock.ACTION_SHOW_ALARMS), 0);
- }
}
}
@@ -431,7 +322,7 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
@Override
public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
if (picture != null &&
- UserManager.get(mContext).isGuestUser(ActivityManager.getCurrentUser()) &&
+ UserManager.get(mContext).isGuestUser(KeyguardUpdateMonitor.getCurrentUser()) &&
!(picture instanceof UserIconDrawable)) {
picture = picture.getConstantState().newDrawable(mContext.getResources()).mutate();
picture.setColorFilter(
diff --git a/com/android/systemui/qs/QSFragment.java b/com/android/systemui/qs/QSFragment.java
index 4a91ee02..cdf0c0f3 100644
--- a/com/android/systemui/qs/QSFragment.java
+++ b/com/android/systemui/qs/QSFragment.java
@@ -63,6 +63,7 @@ public class QSFragment extends Fragment implements QS {
private QSContainerImpl mContainer;
private int mLayoutDirection;
private QSFooter mFooter;
+ private float mLastQSExpansion = -1;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -227,6 +228,7 @@ public class QSFragment extends Fragment implements QS {
public void setKeyguardShowing(boolean keyguardShowing) {
if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing);
mKeyguardShowing = keyguardShowing;
+ mLastQSExpansion = -1;
if (mQSAnimator != null) {
mQSAnimator.setOnKeyguard(keyguardShowing);
@@ -268,6 +270,10 @@ public class QSFragment extends Fragment implements QS {
getView().setTranslationY(mKeyguardShowing ? (translationScaleY * height)
: headerTranslation);
}
+ if (expansion == mLastQSExpansion) {
+ return;
+ }
+ mLastQSExpansion = expansion;
mHeader.setExpansion(mKeyguardShowing ? 1 : expansion);
mFooter.setExpansion(mKeyguardShowing ? 1 : expansion);
int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom()
diff --git a/com/android/systemui/recents/RecentsActivity.java b/com/android/systemui/recents/RecentsActivity.java
index 9aecc686..06dfd183 100644
--- a/com/android/systemui/recents/RecentsActivity.java
+++ b/com/android/systemui/recents/RecentsActivity.java
@@ -46,7 +46,7 @@ import com.android.internal.colorextraction.ColorExtractor;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.keyguard.LatencyTracker;
+import com.android.internal.util.LatencyTracker;
import com.android.systemui.DejankUtils;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
@@ -709,7 +709,6 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD
(event.launchTask == null || launchToTaskId != event.launchTask.key.id)) {
ActivityManagerWrapper am = ActivityManagerWrapper.getInstance();
am.cancelWindowTransition(launchState.launchedToTaskId);
- am.cancelThumbnailTransition(getTaskId());
}
}
diff --git a/com/android/systemui/recents/RecentsImpl.java b/com/android/systemui/recents/RecentsImpl.java
index 663f2067..8359690b 100644
--- a/com/android/systemui/recents/RecentsImpl.java
+++ b/com/android/systemui/recents/RecentsImpl.java
@@ -24,6 +24,8 @@ import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASO
import android.app.ActivityManager;
import android.app.ActivityOptions;
+import android.app.KeyguardManager;
+import android.app.trust.TrustManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
@@ -32,6 +34,7 @@ import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
+import android.os.AsyncTask.Status;
import android.os.Handler;
import android.os.SystemClock;
import android.util.ArraySet;
@@ -225,6 +228,7 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
}
};
+ private TrustManager mTrustManager;
protected Context mContext;
protected Handler mHandler;
TaskStackListenerImpl mTaskStackListener;
@@ -271,6 +275,8 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
// Initialize the static configuration resources
mDummyStackView = new TaskStackView(mContext);
reloadResources();
+
+ mTrustManager = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
}
public void onBootCompleted() {
@@ -309,8 +315,7 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
* {@link Recents#onBusEvent(ScreenPinningRequestEvent)}.
*/
public void onStartScreenPinning(Context context, int taskId) {
- SystemUIApplication app = (SystemUIApplication) context;
- StatusBar statusBar = app.getComponent(StatusBar.class);
+ final StatusBar statusBar = getStatusBar();
if (statusBar != null) {
statusBar.showScreenPinningRequest(taskId, false);
}
@@ -350,8 +355,8 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
if (forceVisible || !ssp.isRecentsActivityVisible(isHomeStackVisible)) {
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
- startRecentsActivity(runningTask, isHomeStackVisible.value || fromHome, animate,
- growTarget);
+ startRecentsActivityAndDismissKeyguardIfNeeded(runningTask,
+ isHomeStackVisible.value || fromHome, animate, growTarget);
}
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to launch RecentsActivity", e);
@@ -442,8 +447,8 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
// Otherwise, start the recents activity
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
- startRecentsActivity(runningTask, isHomeStackVisible.value, true /* animate */,
- growTarget);
+ startRecentsActivityAndDismissKeyguardIfNeeded(runningTask,
+ isHomeStackVisible.value, true /* animate */, growTarget);
// Only close the other system windows if we are actually showing recents
ActivityManagerWrapper.getInstance().closeSystemWindows(
@@ -462,6 +467,12 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
return;
}
+ // Skip preloading recents when keyguard is showing
+ final StatusBar statusBar = getStatusBar();
+ if (statusBar != null && statusBar.isKeyguardShowing()) {
+ return;
+ }
+
// Preload only the raw task list into a new load plan (which will be consumed by the
// RecentsActivity) only if there is a task to animate to. Post this to ensure that we
// don't block the touch feedback on the nav bar button which triggers this.
@@ -942,9 +953,27 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
}
/**
- * Shows the recents activity
+ * Shows the recents activity after dismissing the keyguard if visible
*/
- protected void startRecentsActivity(ActivityManager.RunningTaskInfo runningTask,
+ protected void startRecentsActivityAndDismissKeyguardIfNeeded(
+ final ActivityManager.RunningTaskInfo runningTask, final boolean isHomeStackVisible,
+ final boolean animate, final int growTarget) {
+ // Preload only if device for current user is unlocked
+ final StatusBar statusBar = getStatusBar();
+ if (statusBar != null && statusBar.isKeyguardShowing()) {
+ statusBar.executeRunnableDismissingKeyguard(() -> {
+ // Flush trustmanager before checking device locked per user when preloading
+ mTrustManager.reportKeyguardShowingChanged();
+ mHandler.post(() -> startRecentsActivity(runningTask, isHomeStackVisible,
+ animate, growTarget));
+ }, null, true /* dismissShade */, false /* afterKeyguardGone */,
+ true /* deferred */);
+ } else {
+ startRecentsActivity(runningTask, isHomeStackVisible, animate, growTarget);
+ }
+ }
+
+ private void startRecentsActivity(ActivityManager.RunningTaskInfo runningTask,
boolean isHomeStackVisible, boolean animate, int growTarget) {
RecentsTaskLoader loader = Recents.getTaskLoader();
RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
@@ -1033,6 +1062,10 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
return result;
}
+ private StatusBar getStatusBar() {
+ return ((SystemUIApplication) mContext).getComponent(StatusBar.class);
+ }
+
/**
* Starts the recents activity.
*/
diff --git a/com/android/systemui/recents/events/component/ExpandPipEvent.java b/com/android/systemui/recents/events/component/ExpandPipEvent.java
index 8fe4975f..37266f6f 100644
--- a/com/android/systemui/recents/events/component/ExpandPipEvent.java
+++ b/com/android/systemui/recents/events/component/ExpandPipEvent.java
@@ -22,5 +22,4 @@ import com.android.systemui.recents.events.EventBus;
* This is sent when the PiP should be expanded due to being relaunched.
*/
public class ExpandPipEvent extends EventBus.Event {
- public final boolean clearThumbnailWindows = true;
}
diff --git a/com/android/systemui/recents/misc/SystemServicesProxy.java b/com/android/systemui/recents/misc/SystemServicesProxy.java
index 2d3080b1..130a5e31 100644
--- a/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -267,7 +267,7 @@ public class SystemServicesProxy {
try {
return mIam.setTaskWindowingModeSplitScreenPrimary(taskId, createMode, true /* onTop */,
- false /* animate */, initialBounds);
+ false /* animate */, initialBounds, true /* showRecents */);
} catch (RemoteException e) {
e.printStackTrace();
}
diff --git a/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
index bfaa8cdb..c91cdfc3 100644
--- a/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -256,6 +256,9 @@ class TaskStackViewTouchHandler implements SwipeHelper.Callback {
}
case MotionEvent.ACTION_MOVE: {
int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (activePointerIndex == -1) {
+ break;
+ }
int y = (int) ev.getY(activePointerIndex);
int x = (int) ev.getX(activePointerIndex);
if (!mIsScrolling) {
diff --git a/com/android/systemui/screenshot/GlobalScreenshot.java b/com/android/systemui/screenshot/GlobalScreenshot.java
index 991c3c83..6db46b59 100644
--- a/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -38,7 +38,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
-import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Matrix;
@@ -49,7 +48,6 @@ import android.graphics.Rect;
import android.media.MediaActionSound;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.Bundle;
import android.os.Environment;
import android.os.PowerManager;
import android.os.Process;
@@ -59,10 +57,13 @@ import android.provider.MediaStore;
import android.util.DisplayMetrics;
import android.util.Slog;
import android.view.Display;
+import android.view.DisplayListCanvas;
import android.view.LayoutInflater;
import android.view.MotionEvent;
+import android.view.RenderNode;
import android.view.Surface;
import android.view.SurfaceControl;
+import android.view.ThreadedRenderer;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -154,7 +155,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
int previewWidth = data.previewWidth;
int previewHeight = data.previewheight;
- Canvas c = new Canvas();
Paint paint = new Paint();
ColorMatrix desat = new ColorMatrix();
desat.setSaturation(0.25f);
@@ -162,23 +162,17 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
Matrix matrix = new Matrix();
int overlayColor = 0x40FFFFFF;
- Bitmap picture = Bitmap.createBitmap(previewWidth, previewHeight, data.image.getConfig());
matrix.setTranslate((previewWidth - mImageWidth) / 2, (previewHeight - mImageHeight) / 2);
- c.setBitmap(picture);
- c.drawBitmap(data.image, matrix, paint);
- c.drawColor(overlayColor);
- c.setBitmap(null);
+ Bitmap picture = generateAdjustedHwBitmap(data.image, previewWidth, previewHeight, matrix,
+ paint, overlayColor);
// Note, we can't use the preview for the small icon, since it is non-square
float scale = (float) iconSize / Math.min(mImageWidth, mImageHeight);
- Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, data.image.getConfig());
matrix.setScale(scale, scale);
matrix.postTranslate((iconSize - (scale * mImageWidth)) / 2,
(iconSize - (scale * mImageHeight)) / 2);
- c.setBitmap(icon);
- c.drawBitmap(data.image, matrix, paint);
- c.drawColor(overlayColor);
- c.setBitmap(null);
+ Bitmap icon = generateAdjustedHwBitmap(data.image, iconSize, iconSize, matrix, paint,
+ overlayColor);
// Show the intermediate notification
mTickerAddSpace = !mTickerAddSpace;
@@ -232,6 +226,22 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
mNotificationStyle.bigLargeIcon((Bitmap) null);
}
+ /**
+ * Generates a new hardware bitmap with specified values, copying the content from the passed
+ * in bitmap.
+ */
+ private Bitmap generateAdjustedHwBitmap(Bitmap bitmap, int width, int height, Matrix matrix,
+ Paint paint, int color) {
+ RenderNode node = RenderNode.create("ScreenshotCanvas", null);
+ node.setLeftTopRightBottom(0, 0, width, height);
+ node.setClipToBounds(false);
+ DisplayListCanvas canvas = node.start(width, height);
+ canvas.drawColor(color);
+ canvas.drawBitmap(bitmap, matrix, paint);
+ node.end(canvas);
+ return ThreadedRenderer.createHardwareBitmap(node, width, height);
+ }
+
@Override
protected Void doInBackground(Void... params) {
if (isCancelled()) {
@@ -557,25 +567,14 @@ class GlobalScreenshot {
/**
* Takes a screenshot of the current display and shows an animation.
*/
- void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible,
- int x, int y, int width, int height) {
- // We need to orient the screenshot correctly (and the Surface api seems to take screenshots
- // only in the natural orientation of the device :!)
- mDisplay.getRealMetrics(mDisplayMetrics);
- float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
- float degrees = getDegreesForRotation(mDisplay.getRotation());
- boolean requiresRotation = (degrees > 0);
- if (requiresRotation) {
- // Get the dimensions of the device in its native orientation
- mDisplayMatrix.reset();
- mDisplayMatrix.preRotate(-degrees);
- mDisplayMatrix.mapPoints(dims);
- dims[0] = Math.abs(dims[0]);
- dims[1] = Math.abs(dims[1]);
- }
+ private void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible,
+ Rect crop) {
+ int rot = mDisplay.getRotation();
+ int width = crop.width();
+ int height = crop.height();
// Take the screenshot
- mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
+ mScreenBitmap = SurfaceControl.screenshot(crop, width, height, rot);
if (mScreenBitmap == null) {
notifyScreenshotError(mContext, mNotificationManager,
R.string.screenshot_failed_to_capture_text);
@@ -583,29 +582,6 @@ class GlobalScreenshot {
return;
}
- if (requiresRotation) {
- // Rotate the screenshot to the current orientation
- Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
- mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888,
- mScreenBitmap.hasAlpha(), mScreenBitmap.getColorSpace());
- Canvas c = new Canvas(ss);
- c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
- c.rotate(degrees);
- c.translate(-dims[0] / 2, -dims[1] / 2);
- c.drawBitmap(mScreenBitmap, 0, 0, null);
- c.setBitmap(null);
- // Recycle the previous bitmap
- mScreenBitmap.recycle();
- mScreenBitmap = ss;
- }
-
- if (width != mDisplayMetrics.widthPixels || height != mDisplayMetrics.heightPixels) {
- // Crop the screenshot to selected region
- Bitmap cropped = Bitmap.createBitmap(mScreenBitmap, x, y, width, height);
- mScreenBitmap.recycle();
- mScreenBitmap = cropped;
- }
-
// Optimizations
mScreenBitmap.setHasAlpha(false);
mScreenBitmap.prepareToDraw();
@@ -617,8 +593,8 @@ class GlobalScreenshot {
void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
mDisplay.getRealMetrics(mDisplayMetrics);
- takeScreenshot(finisher, statusBarVisible, navBarVisible, 0, 0, mDisplayMetrics.widthPixels,
- mDisplayMetrics.heightPixels);
+ takeScreenshot(finisher, statusBarVisible, navBarVisible,
+ new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
}
/**
@@ -648,7 +624,7 @@ class GlobalScreenshot {
mScreenshotLayout.post(new Runnable() {
public void run() {
takeScreenshot(finisher, statusBarVisible, navBarVisible,
- rect.left, rect.top, rect.width(), rect.height());
+ rect);
}
});
}
diff --git a/com/android/systemui/shared/system/ActivityManagerWrapper.java b/com/android/systemui/shared/system/ActivityManagerWrapper.java
index eb2d12ed..1c99d384 100644
--- a/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -413,15 +413,4 @@ public class ActivityManagerWrapper {
Log.w(TAG, "Failed to cancel window transition for task=" + taskId, e);
}
}
-
- /**
- * Cancels the current thumbnail transtion to/from Recents for the given task id.
- */
- public void cancelThumbnailTransition(int taskId) {
- try {
- ActivityManager.getService().cancelTaskThumbnailTransition(taskId);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to cancel window transition for task=" + taskId, e);
- }
- }
}
diff --git a/com/android/systemui/shared/system/TaskStackChangeListener.java b/com/android/systemui/shared/system/TaskStackChangeListener.java
index 17cb0d80..7db3ac6e 100644
--- a/com/android/systemui/shared/system/TaskStackChangeListener.java
+++ b/com/android/systemui/shared/system/TaskStackChangeListener.java
@@ -43,6 +43,9 @@ public abstract class TaskStackChangeListener {
public void onActivityDismissingDockedStack() { }
public void onActivityLaunchOnSecondaryDisplayFailed() { }
public void onTaskProfileLocked(int taskId, int userId) { }
+ public void onTaskRemoved(int taskId) { }
+ public void onTaskMovedToFront(int taskId) { }
+ public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) { }
/**
* Checks that the current user matches the process. Since
diff --git a/com/android/systemui/shared/system/TaskStackChangeListeners.java b/com/android/systemui/shared/system/TaskStackChangeListeners.java
index 81c37a95..857e0ead 100644
--- a/com/android/systemui/shared/system/TaskStackChangeListeners.java
+++ b/com/android/systemui/shared/system/TaskStackChangeListeners.java
@@ -145,6 +145,23 @@ public class TaskStackChangeListeners extends TaskStackListener {
mHandler.obtainMessage(H.ON_TASK_SNAPSHOT_CHANGED, taskId, 0, snapshot).sendToTarget();
}
+ @Override
+ public void onTaskRemoved(int taskId) throws RemoteException {
+ mHandler.obtainMessage(H.ON_TASK_REMOVED, taskId, 0).sendToTarget();
+ }
+
+ @Override
+ public void onTaskMovedToFront(int taskId) throws RemoteException {
+ mHandler.obtainMessage(H.ON_TASK_MOVED_TO_FRONT, taskId, 0).sendToTarget();
+ }
+
+ @Override
+ public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation)
+ throws RemoteException {
+ mHandler.obtainMessage(H.ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE, taskId,
+ requestedOrientation).sendToTarget();
+ }
+
private final class H extends Handler {
private static final int ON_TASK_STACK_CHANGED = 1;
private static final int ON_TASK_SNAPSHOT_CHANGED = 2;
@@ -157,6 +174,10 @@ public class TaskStackChangeListeners extends TaskStackListener {
private static final int ON_PINNED_STACK_ANIMATION_STARTED = 9;
private static final int ON_ACTIVITY_UNPINNED = 10;
private static final int ON_ACTIVITY_LAUNCH_ON_SECONDARY_DISPLAY_FAILED = 11;
+ private static final int ON_TASK_REMOVED = 12;
+ private static final int ON_TASK_MOVED_TO_FRONT = 13;
+ private static final int ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE = 14;
+
public H(Looper looper) {
super(looper);
@@ -241,6 +262,25 @@ public class TaskStackChangeListeners extends TaskStackListener {
}
break;
}
+ case ON_TASK_REMOVED: {
+ for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
+ mTaskStackListeners.get(i).onTaskRemoved(msg.arg1);
+ }
+ break;
+ }
+ case ON_TASK_MOVED_TO_FRONT: {
+ for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
+ mTaskStackListeners.get(i).onTaskMovedToFront(msg.arg1);
+ }
+ break;
+ }
+ case ON_ACTIVITY_REQUESTED_ORIENTATION_CHANGE: {
+ for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
+ mTaskStackListeners.get(i).onActivityRequestedOrientationChanged(
+ msg.arg1, msg.arg2);
+ }
+ break;
+ }
}
}
}
diff --git a/com/android/systemui/statusbar/ExpandableNotificationRow.java b/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 23d9caee..f53eb489 100644
--- a/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -112,6 +112,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private int mNotificationMaxHeight;
private int mNotificationAmbientHeight;
private int mIncreasedPaddingBetweenElements;
+ private boolean mMustStayOnScreen;
/** Does this row contain layouts that can adapt to row expansion */
private boolean mExpandable;
@@ -205,7 +206,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private OnClickListener mExpandClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
- if (!mShowingPublic && (!mIsLowPriority || isExpanded())
+ if (!shouldShowPublic() && (!mIsLowPriority || isExpanded())
&& mGroupManager.isSummaryOfGroup(mStatusBarNotification)) {
mGroupExpansionChanging = true;
final boolean wasExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification);
@@ -491,6 +492,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
notifyHeightChanged(false /* needsAnimation */);
}
if (isHeadsUp) {
+ mMustStayOnScreen = true;
setAboveShelf(true);
} else if (isAboveShelf() != wasAboveShelf) {
mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
@@ -517,6 +519,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
addChildNotification(row, -1);
}
+ @Override
+ public void setHeadsUpIsVisible() {
+ super.setHeadsUpIsVisible();
+ mMustStayOnScreen = false;
+ }
+
/**
* Add a child notification to this view.
*
@@ -782,7 +790,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
* {@link #getNotificationHeader()} in case it is a low-priority group.
*/
public NotificationHeaderView getVisibleNotificationHeader() {
- if (mIsSummaryWithChildren && !mShowingPublic) {
+ if (mIsSummaryWithChildren && !shouldShowPublic()) {
return mChildrenContainer.getVisibleHeader();
}
return getShowingLayout().getVisibleNotificationHeader();
@@ -1504,10 +1512,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
private void updateChildrenVisibility() {
- mPrivateLayout.setVisibility(!mShowingPublic && !mIsSummaryWithChildren ? VISIBLE
+ mPrivateLayout.setVisibility(!shouldShowPublic() && !mIsSummaryWithChildren ? VISIBLE
: INVISIBLE);
if (mChildrenContainer != null) {
- mChildrenContainer.setVisibility(!mShowingPublic && mIsSummaryWithChildren ? VISIBLE
+ mChildrenContainer.setVisibility(!shouldShowPublic() && mIsSummaryWithChildren ? VISIBLE
: INVISIBLE);
}
// The limits might have changed if the view suddenly became a group or vice versa
@@ -1558,7 +1566,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public boolean isExpandable() {
- if (mIsSummaryWithChildren && !mShowingPublic) {
+ if (mIsSummaryWithChildren && !shouldShowPublic()) {
return !mChildrenExpanded;
}
return mEnableNonGroupedNotificationExpand && mExpandable;
@@ -1603,7 +1611,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
*/
public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
mFalsingManager.setNotificationExpanded();
- if (mIsSummaryWithChildren && !mShowingPublic && allowChildExpansion
+ if (mIsSummaryWithChildren && !shouldShowPublic() && allowChildExpansion
&& !mChildrenContainer.showingAsLowPriority()) {
final boolean wasExpanded = mGroupManager.isGroupExpanded(mStatusBarNotification);
mGroupManager.setGroupExpanded(mStatusBarNotification, userExpanded);
@@ -1898,7 +1906,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE);
updateChildrenVisibility();
} else {
- animateShowingPublic(delay, duration);
+ animateShowingPublic(delay, duration, mShowingPublic);
}
NotificationContentView showingLayout = getShowingLayout();
showingLayout.updateBackgroundColor(animated);
@@ -1908,13 +1916,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mShowingPublicInitialized = true;
}
- private void animateShowingPublic(long delay, long duration) {
+ private void animateShowingPublic(long delay, long duration, boolean showingPublic) {
View[] privateViews = mIsSummaryWithChildren
? new View[] {mChildrenContainer}
: new View[] {mPrivateLayout};
View[] publicViews = new View[] {mPublicLayout};
- View[] hiddenChildren = mShowingPublic ? privateViews : publicViews;
- View[] shownChildren = mShowingPublic ? publicViews : privateViews;
+ View[] hiddenChildren = showingPublic ? privateViews : publicViews;
+ View[] shownChildren = showingPublic ? publicViews : privateViews;
for (final View hiddenView : hiddenChildren) {
hiddenView.setVisibility(View.VISIBLE);
hiddenView.animate().cancel();
@@ -1942,7 +1950,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
@Override
public boolean mustStayOnScreen() {
- return mIsHeadsUp;
+ return mIsHeadsUp && mMustStayOnScreen;
}
/**
@@ -1951,7 +1959,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
* see {@link #isClearable()}.
*/
public boolean canViewBeDismissed() {
- return isClearable() && (!mShowingPublic || !mSensitiveHiddenInGeneral);
+ return isClearable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral);
+ }
+
+ private boolean shouldShowPublic() {
+ return mSensitive && mHideSensitiveForIntrinsicHeight;
}
public void makeActionsVisibile() {
@@ -1997,7 +2009,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
@Override
public boolean isContentExpandable() {
- if (mIsSummaryWithChildren && !mShowingPublic) {
+ if (mIsSummaryWithChildren && !shouldShowPublic()) {
return true;
}
NotificationContentView showingLayout = getShowingLayout();
@@ -2006,7 +2018,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
@Override
protected View getContentView() {
- if (mIsSummaryWithChildren && !mShowingPublic) {
+ if (mIsSummaryWithChildren && !shouldShowPublic()) {
return mChildrenContainer;
}
return getShowingLayout();
@@ -2071,7 +2083,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
@Override
public int getMaxContentHeight() {
- if (mIsSummaryWithChildren && !mShowingPublic) {
+ if (mIsSummaryWithChildren && !shouldShowPublic()) {
return mChildrenContainer.getMaxContentHeight();
}
NotificationContentView showingLayout = getShowingLayout();
@@ -2085,7 +2097,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
} else if (!ignoreTemporaryStates && isHeadsUpAllowed() && mIsHeadsUp
&& mHeadsUpManager.isTrackingHeadsUp()) {
return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
- } else if (mIsSummaryWithChildren && !isGroupExpanded() && !mShowingPublic) {
+ } else if (mIsSummaryWithChildren && !isGroupExpanded() && !shouldShowPublic()) {
return mChildrenContainer.getMinHeight();
} else if (!ignoreTemporaryStates && isHeadsUpAllowed() && mIsHeadsUp) {
return mHeadsUpHeight;
@@ -2096,7 +2108,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
@Override
public int getCollapsedHeight() {
- if (mIsSummaryWithChildren && !mShowingPublic) {
+ if (mIsSummaryWithChildren && !shouldShowPublic()) {
return mChildrenContainer.getCollapsedHeight();
}
return getMinHeight();
@@ -2136,7 +2148,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public NotificationContentView getShowingLayout() {
- return mShowingPublic ? mPublicLayout : mPrivateLayout;
+ return shouldShowPublic() ? mPublicLayout : mPrivateLayout;
}
public void setLegacy(boolean legacy) {
@@ -2242,7 +2254,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (header != null && header.isInTouchRect(x - getTranslation(), y)) {
return true;
}
- if ((!mIsSummaryWithChildren || mShowingPublic)
+ if ((!mIsSummaryWithChildren || shouldShowPublic())
&& getShowingLayout().disallowSingleClick(x, y)) {
return true;
}
@@ -2272,7 +2284,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (canViewBeDismissed()) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS);
}
- boolean expandable = mShowingPublic;
+ boolean expandable = shouldShowPublic();
boolean isExpanded = false;
if (!expandable) {
if (mIsSummaryWithChildren) {
diff --git a/com/android/systemui/statusbar/ExpandableOutlineView.java b/com/android/systemui/statusbar/ExpandableOutlineView.java
index b3d6e32d..db19d2f0 100644
--- a/com/android/systemui/statusbar/ExpandableOutlineView.java
+++ b/com/android/systemui/statusbar/ExpandableOutlineView.java
@@ -80,10 +80,21 @@ public abstract class ExpandableOutlineView extends ExpandableView {
private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
- Path clipPath = getClipPath();
- if (clipPath != null && clipPath.isConvex()) {
- // The path might not be convex in border cases where the view is small and clipped
- outline.setConvexPath(clipPath);
+ if (!mCustomOutline && mCurrentTopRoundness == 0.0f
+ && mCurrentBottomRoundness == 0.0f && !mAlwaysRoundBothCorners) {
+ int translation = mShouldTranslateContents ? (int) getTranslation() : 0;
+ int left = Math.max(translation + mCurrentSidePaddings, mCurrentSidePaddings);
+ int top = mClipTopAmount + mBackgroundTop;
+ int right = getWidth() - mCurrentSidePaddings + Math.min(translation, 0);
+ int bottom = Math.max(getActualHeight() - mClipBottomAmount, top);
+ outline.setRect(left, top, right, bottom);
+ } else {
+ Path clipPath = getClipPath();
+ if (clipPath != null && clipPath.isConvex()) {
+ // The path might not be convex in border cases where the view is small and
+ // clipped
+ outline.setConvexPath(clipPath);
+ }
}
outline.setAlpha(mOutlineAlpha);
}
diff --git a/com/android/systemui/statusbar/ExpandableView.java b/com/android/systemui/statusbar/ExpandableView.java
index 18b98602..f762513d 100644
--- a/com/android/systemui/statusbar/ExpandableView.java
+++ b/com/android/systemui/statusbar/ExpandableView.java
@@ -478,6 +478,9 @@ public abstract class ExpandableView extends FrameLayout {
return false;
}
+ public void setHeadsUpIsVisible() {
+ }
+
public boolean isChildInGroup() {
return false;
}
diff --git a/com/android/systemui/statusbar/NotificationEntryManager.java b/com/android/systemui/statusbar/NotificationEntryManager.java
new file mode 100644
index 00000000..6bbd09f7
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationEntryManager.java
@@ -0,0 +1,960 @@
+/*
+ * 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 com.android.systemui.statusbar;
+
+import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT;
+import static com.android.systemui.statusbar.NotificationRemoteInputManager
+ .FORCE_REMOTE_INPUT_HISTORY;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.os.Build;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationStats;
+import android.service.notification.StatusBarNotification;
+import android.util.ArraySet;
+import android.util.EventLog;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.util.NotificationMessagingUtil;
+import com.android.systemui.DejankUtils;
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.EventLogTags;
+import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.R;
+import com.android.systemui.UiOffloadThread;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.statusbar.notification.InflationException;
+import com.android.systemui.statusbar.notification.NotificationInflater;
+import com.android.systemui.statusbar.notification.RowInflaterTask;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.util.leak.LeakDetector;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * NotificationEntryManager is responsible for the adding, removing, and updating of notifications.
+ * It also handles tasks such as their inflation and their interaction with other
+ * Notification.*Manager objects.
+ */
+public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,
+ ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,
+ VisualStabilityManager.Callback {
+ private static final String TAG = "NotificationEntryManager";
+ protected static final boolean DEBUG = false;
+ protected static final boolean ENABLE_HEADS_UP = true;
+ protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
+
+ protected final NotificationMessagingUtil mMessagingUtil;
+ protected final Context mContext;
+ protected final HashMap<String, NotificationData.Entry> mPendingNotifications = new HashMap<>();
+ protected final NotificationClicker mNotificationClicker = new NotificationClicker();
+ protected final ArraySet<NotificationData.Entry> mHeadsUpEntriesToRemoveOnSwitch =
+ new ArraySet<>();
+
+ // Dependencies:
+ protected final NotificationLockscreenUserManager mLockscreenUserManager =
+ Dependency.get(NotificationLockscreenUserManager.class);
+ protected final NotificationGroupManager mGroupManager =
+ Dependency.get(NotificationGroupManager.class);
+ protected final NotificationGutsManager mGutsManager =
+ Dependency.get(NotificationGutsManager.class);
+ protected final NotificationRemoteInputManager mRemoteInputManager =
+ Dependency.get(NotificationRemoteInputManager.class);
+ protected final NotificationMediaManager mMediaManager =
+ Dependency.get(NotificationMediaManager.class);
+ protected final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
+ protected final DeviceProvisionedController mDeviceProvisionedController =
+ Dependency.get(DeviceProvisionedController.class);
+ protected final VisualStabilityManager mVisualStabilityManager =
+ Dependency.get(VisualStabilityManager.class);
+ protected final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
+ protected final ForegroundServiceController mForegroundServiceController =
+ Dependency.get(ForegroundServiceController.class);
+ protected final NotificationListener mNotificationListener =
+ Dependency.get(NotificationListener.class);
+
+ protected IStatusBarService mBarService;
+ protected NotificationPresenter mPresenter;
+ protected Callback mCallback;
+ protected PowerManager mPowerManager;
+ protected SystemServicesProxy mSystemServicesProxy;
+ protected NotificationListenerService.RankingMap mLatestRankingMap;
+ protected HeadsUpManager mHeadsUpManager;
+ protected NotificationData mNotificationData;
+ protected ContentObserver mHeadsUpObserver;
+ protected boolean mUseHeadsUp = false;
+ protected boolean mDisableNotificationAlerts;
+ protected NotificationListContainer mListContainer;
+
+ private final class NotificationClicker implements View.OnClickListener {
+
+ @Override
+ public void onClick(final View v) {
+ if (!(v instanceof ExpandableNotificationRow)) {
+ Log.e(TAG, "NotificationClicker called on a view that is not a notification row.");
+ return;
+ }
+
+ mPresenter.wakeUpIfDozing(SystemClock.uptimeMillis(), v);
+
+ final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
+ final StatusBarNotification sbn = row.getStatusBarNotification();
+ if (sbn == null) {
+ Log.e(TAG, "NotificationClicker called on an unclickable notification,");
+ return;
+ }
+
+ // Check if the notification is displaying the menu, if so slide notification back
+ if (row.getProvider() != null && row.getProvider().isMenuVisible()) {
+ row.animateTranslateNotification(0);
+ return;
+ }
+
+ // Mark notification for one frame.
+ row.setJustClicked(true);
+ DejankUtils.postAfterTraversal(() -> row.setJustClicked(false));
+
+ mCallback.onNotificationClicked(sbn, row);
+ }
+
+ public void register(ExpandableNotificationRow row, StatusBarNotification sbn) {
+ Notification notification = sbn.getNotification();
+ if (notification.contentIntent != null || notification.fullScreenIntent != null) {
+ row.setOnClickListener(this);
+ } else {
+ row.setOnClickListener(null);
+ }
+ }
+ }
+
+ private final DeviceProvisionedController.DeviceProvisionedListener
+ mDeviceProvisionedListener =
+ new DeviceProvisionedController.DeviceProvisionedListener() {
+ @Override
+ public void onDeviceProvisionedChanged() {
+ updateNotifications();
+ }
+ };
+
+ public NotificationListenerService.RankingMap getLatestRankingMap() {
+ return mLatestRankingMap;
+ }
+
+ public void setLatestRankingMap(NotificationListenerService.RankingMap latestRankingMap) {
+ mLatestRankingMap = latestRankingMap;
+ }
+
+ public void setDisableNotificationAlerts(boolean disableNotificationAlerts) {
+ mDisableNotificationAlerts = disableNotificationAlerts;
+ mHeadsUpObserver.onChange(true);
+ }
+
+ public void destroy() {
+ mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener);
+ }
+
+ public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
+ if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) {
+ removeNotification(entry.key, getLatestRankingMap());
+ mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
+ if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) {
+ setLatestRankingMap(null);
+ }
+ } else {
+ updateNotificationRanking(null);
+ }
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("NotificationEntryManager state:");
+ pw.print(" mPendingNotifications=");
+ if (mPendingNotifications.size() == 0) {
+ pw.println("null");
+ } else {
+ for (NotificationData.Entry entry : mPendingNotifications.values()) {
+ pw.println(entry.notification);
+ }
+ }
+ pw.print(" mUseHeadsUp=");
+ pw.println(mUseHeadsUp);
+ }
+
+ public NotificationEntryManager(Context context) {
+ mContext = context;
+ mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ mBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ mMessagingUtil = new NotificationMessagingUtil(context);
+ mSystemServicesProxy = SystemServicesProxy.getInstance(mContext);
+ }
+
+ public void setUpWithPresenter(NotificationPresenter presenter,
+ NotificationListContainer listContainer, Callback callback,
+ HeadsUpManager headsUpManager) {
+ mPresenter = presenter;
+ mCallback = callback;
+ mNotificationData = new NotificationData(presenter);
+ mHeadsUpManager = headsUpManager;
+ mNotificationData.setHeadsUpManager(mHeadsUpManager);
+ mListContainer = listContainer;
+
+ mHeadsUpObserver = new ContentObserver(mPresenter.getHandler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ boolean wasUsing = mUseHeadsUp;
+ mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts
+ && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt(
+ mContext.getContentResolver(),
+ Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
+ Settings.Global.HEADS_UP_OFF);
+ Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled"));
+ if (wasUsing != mUseHeadsUp) {
+ if (!mUseHeadsUp) {
+ Log.d(TAG,
+ "dismissing any existing heads up notification on disable event");
+ mHeadsUpManager.releaseAllImmediately();
+ }
+ }
+ }
+ };
+
+ if (ENABLE_HEADS_UP) {
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED),
+ true,
+ mHeadsUpObserver);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
+ mHeadsUpObserver);
+ }
+
+ mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
+
+ mHeadsUpObserver.onChange(true); // set up
+ }
+
+ public NotificationData getNotificationData() {
+ return mNotificationData;
+ }
+
+ public ExpandableNotificationRow.LongPressListener getNotificationLongClicker() {
+ return mGutsManager::openGuts;
+ }
+
+ @Override
+ public void logNotificationExpansion(String key, boolean userAction, boolean expanded) {
+ mUiOffloadThread.submit(() -> {
+ try {
+ mBarService.onNotificationExpansionChanged(key, userAction, expanded);
+ } catch (RemoteException e) {
+ // Ignore.
+ }
+ });
+ }
+
+ @Override
+ public void onReorderingAllowed() {
+ updateNotifications();
+ }
+
+ private boolean shouldSuppressFullScreenIntent(String key) {
+ if (mPresenter.isDeviceInVrMode()) {
+ return true;
+ }
+
+ if (mPowerManager.isInteractive()) {
+ return mNotificationData.shouldSuppressScreenOn(key);
+ } else {
+ return mNotificationData.shouldSuppressScreenOff(key);
+ }
+ }
+
+ private void inflateViews(NotificationData.Entry entry, ViewGroup parent) {
+ PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
+ entry.notification.getUser().getIdentifier());
+
+ final StatusBarNotification sbn = entry.notification;
+ if (entry.row != null) {
+ entry.reset();
+ updateNotification(entry, pmUser, sbn, entry.row);
+ } else {
+ new RowInflaterTask().inflate(mContext, parent, entry,
+ row -> {
+ bindRow(entry, pmUser, sbn, row);
+ updateNotification(entry, pmUser, sbn, row);
+ });
+ }
+ }
+
+ private void bindRow(NotificationData.Entry entry, PackageManager pmUser,
+ StatusBarNotification sbn, ExpandableNotificationRow row) {
+ row.setExpansionLogger(this, entry.notification.getKey());
+ row.setGroupManager(mGroupManager);
+ row.setHeadsUpManager(mHeadsUpManager);
+ row.setOnExpandClickListener(mPresenter);
+ row.setInflationCallback(this);
+ row.setLongPressListener(getNotificationLongClicker());
+ mRemoteInputManager.bindRow(row);
+
+ // Get the app name.
+ // Note that Notification.Builder#bindHeaderAppName has similar logic
+ // but since this field is used in the guts, it must be accurate.
+ // Therefore we will only show the application label, or, failing that, the
+ // package name. No substitutions.
+ final String pkg = sbn.getPackageName();
+ String appname = pkg;
+ try {
+ final ApplicationInfo info = pmUser.getApplicationInfo(pkg,
+ PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DISABLED_COMPONENTS);
+ if (info != null) {
+ appname = String.valueOf(pmUser.getApplicationLabel(info));
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // Do nothing
+ }
+ row.setAppName(appname);
+ row.setOnDismissRunnable(() ->
+ performRemoveNotification(row.getStatusBarNotification()));
+ row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ if (ENABLE_REMOTE_INPUT) {
+ row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+ }
+
+ mCallback.onBindRow(entry, pmUser, sbn, row);
+ }
+
+ public void performRemoveNotification(StatusBarNotification n) {
+ NotificationData.Entry entry = mNotificationData.get(n.getKey());
+ mRemoteInputManager.onPerformRemoveNotification(n, entry);
+ final String pkg = n.getPackageName();
+ final String tag = n.getTag();
+ final int id = n.getId();
+ final int userId = n.getUserId();
+ try {
+ int dismissalSurface = NotificationStats.DISMISSAL_SHADE;
+ if (isHeadsUp(n.getKey())) {
+ dismissalSurface = NotificationStats.DISMISSAL_PEEK;
+ } else if (mListContainer.hasPulsingNotifications()) {
+ dismissalSurface = NotificationStats.DISMISSAL_AOD;
+ }
+ mBarService.onNotificationClear(pkg, tag, id, userId, n.getKey(), dismissalSurface);
+ removeNotification(n.getKey(), null);
+
+ } catch (RemoteException ex) {
+ // system process is dead if we're here.
+ }
+
+ mCallback.onPerformRemoveNotification(n);
+ }
+
+ /**
+ * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService
+ * about the failure.
+ *
+ * WARNING: this will call back into us. Don't hold any locks.
+ */
+ void handleNotificationError(StatusBarNotification n, String message) {
+ removeNotification(n.getKey(), null);
+ try {
+ mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(),
+ n.getInitialPid(), message, n.getUserId());
+ } catch (RemoteException ex) {
+ // The end is nigh.
+ }
+ }
+
+ private void abortExistingInflation(String key) {
+ if (mPendingNotifications.containsKey(key)) {
+ NotificationData.Entry entry = mPendingNotifications.get(key);
+ entry.abortTask();
+ mPendingNotifications.remove(key);
+ }
+ NotificationData.Entry addedEntry = mNotificationData.get(key);
+ if (addedEntry != null) {
+ addedEntry.abortTask();
+ }
+ }
+
+ @Override
+ public void handleInflationException(StatusBarNotification notification, Exception e) {
+ handleNotificationError(notification, e.getMessage());
+ }
+
+ private void addEntry(NotificationData.Entry shadeEntry) {
+ boolean isHeadsUped = shouldPeek(shadeEntry);
+ if (isHeadsUped) {
+ mHeadsUpManager.showNotification(shadeEntry);
+ // Mark as seen immediately
+ setNotificationShown(shadeEntry.notification);
+ }
+ addNotificationViews(shadeEntry);
+ mCallback.onNotificationAdded(shadeEntry);
+ }
+
+ @Override
+ public void onAsyncInflationFinished(NotificationData.Entry entry) {
+ mPendingNotifications.remove(entry.key);
+ // If there was an async task started after the removal, we don't want to add it back to
+ // the list, otherwise we might get leaks.
+ boolean isNew = mNotificationData.get(entry.key) == null;
+ if (isNew && !entry.row.isRemoved()) {
+ addEntry(entry);
+ } else if (!isNew && entry.row.hasLowPriorityStateUpdated()) {
+ mVisualStabilityManager.onLowPriorityUpdated(entry);
+ mPresenter.updateNotificationViews();
+ }
+ entry.row.setLowPriorityStateUpdated(false);
+ }
+
+ @Override
+ public void removeNotification(String key, NotificationListenerService.RankingMap ranking) {
+ boolean deferRemoval = false;
+ abortExistingInflation(key);
+ if (mHeadsUpManager.isHeadsUp(key)) {
+ // A cancel() in response to a remote input shouldn't be delayed, as it makes the
+ // sending look longer than it takes.
+ // Also we should not defer the removal if reordering isn't allowed since otherwise
+ // some notifications can't disappear before the panel is closed.
+ boolean ignoreEarliestRemovalTime = mRemoteInputManager.getController().isSpinning(key)
+ && !FORCE_REMOTE_INPUT_HISTORY
+ || !mVisualStabilityManager.isReorderingAllowed();
+ deferRemoval = !mHeadsUpManager.removeNotification(key, ignoreEarliestRemovalTime);
+ }
+ mMediaManager.onNotificationRemoved(key);
+
+ NotificationData.Entry entry = mNotificationData.get(key);
+ if (FORCE_REMOTE_INPUT_HISTORY && mRemoteInputManager.getController().isSpinning(key)
+ && entry.row != null && !entry.row.isDismissed()) {
+ StatusBarNotification sbn = entry.notification;
+
+ Notification.Builder b = Notification.Builder
+ .recoverBuilder(mContext, sbn.getNotification().clone());
+ CharSequence[] oldHistory = sbn.getNotification().extras
+ .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
+ CharSequence[] newHistory;
+ if (oldHistory == null) {
+ newHistory = new CharSequence[1];
+ } else {
+ newHistory = new CharSequence[oldHistory.length + 1];
+ System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
+ }
+ newHistory[0] = String.valueOf(entry.remoteInputText);
+ b.setRemoteInputHistory(newHistory);
+
+ Notification newNotification = b.build();
+
+ // Undo any compatibility view inflation
+ newNotification.contentView = sbn.getNotification().contentView;
+ newNotification.bigContentView = sbn.getNotification().bigContentView;
+ newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
+
+ StatusBarNotification newSbn = new StatusBarNotification(sbn.getPackageName(),
+ sbn.getOpPkg(),
+ sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(),
+ newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime());
+ boolean updated = false;
+ try {
+ updateNotificationInternal(newSbn, null);
+ updated = true;
+ } catch (InflationException e) {
+ deferRemoval = false;
+ }
+ if (updated) {
+ Log.w(TAG, "Keeping notification around after sending remote input "+ entry.key);
+ mRemoteInputManager.getKeysKeptForRemoteInput().add(entry.key);
+ return;
+ }
+ }
+ if (deferRemoval) {
+ mLatestRankingMap = ranking;
+ mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
+ return;
+ }
+
+ if (mRemoteInputManager.onRemoveNotification(entry)) {
+ mLatestRankingMap = ranking;
+ return;
+ }
+
+ if (entry != null && mGutsManager.getExposedGuts() != null
+ && mGutsManager.getExposedGuts() == entry.row.getGuts()
+ && entry.row.getGuts() != null && !entry.row.getGuts().isLeavebehind()) {
+ Log.w(TAG, "Keeping notification because it's showing guts. " + key);
+ mLatestRankingMap = ranking;
+ mGutsManager.setKeyToRemoveOnGutsClosed(key);
+ return;
+ }
+
+ if (entry != null) {
+ mForegroundServiceController.removeNotification(entry.notification);
+ }
+
+ if (entry != null && entry.row != null) {
+ entry.row.setRemoved();
+ mListContainer.cleanUpViewState(entry.row);
+ }
+ // Let's remove the children if this was a summary
+ handleGroupSummaryRemoved(key);
+ StatusBarNotification old = removeNotificationViews(key, ranking);
+
+ mCallback.onNotificationRemoved(key, old);
+ }
+
+ private StatusBarNotification removeNotificationViews(String key,
+ NotificationListenerService.RankingMap ranking) {
+ NotificationData.Entry entry = mNotificationData.remove(key, ranking);
+ if (entry == null) {
+ Log.w(TAG, "removeNotification for unknown key: " + key);
+ return null;
+ }
+ updateNotifications();
+ Dependency.get(LeakDetector.class).trackGarbage(entry);
+ return entry.notification;
+ }
+
+ /**
+ * Ensures that the group children are cancelled immediately when the group summary is cancelled
+ * instead of waiting for the notification manager to send all cancels. Otherwise this could
+ * lead to flickers.
+ *
+ * This also ensures that the animation looks nice and only consists of a single disappear
+ * animation instead of multiple.
+ * @param key the key of the notification was removed
+ *
+ */
+ private void handleGroupSummaryRemoved(String key) {
+ NotificationData.Entry entry = mNotificationData.get(key);
+ if (entry != null && entry.row != null
+ && entry.row.isSummaryWithChildren()) {
+ if (entry.notification.getOverrideGroupKey() != null && !entry.row.isDismissed()) {
+ // We don't want to remove children for autobundled notifications as they are not
+ // always cancelled. We only remove them if they were dismissed by the user.
+ return;
+ }
+ List<ExpandableNotificationRow> notificationChildren =
+ entry.row.getNotificationChildren();
+ for (int i = 0; i < notificationChildren.size(); i++) {
+ ExpandableNotificationRow row = notificationChildren.get(i);
+ if ((row.getStatusBarNotification().getNotification().flags
+ & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
+ // the child is a foreground service notification which we can't remove!
+ continue;
+ }
+ row.setKeepInParent(true);
+ // we need to set this state earlier as otherwise we might generate some weird
+ // animations
+ row.setRemoved();
+ }
+ }
+ }
+
+ public void updateNotificationsOnDensityOrFontScaleChanged() {
+ ArrayList<NotificationData.Entry> activeNotifications =
+ mNotificationData.getActiveNotifications();
+ for (int i = 0; i < activeNotifications.size(); i++) {
+ NotificationData.Entry entry = activeNotifications.get(i);
+ boolean exposedGuts = mGutsManager.getExposedGuts() != null
+ && entry.row.getGuts() == mGutsManager.getExposedGuts();
+ entry.row.onDensityOrFontScaleChanged();
+ if (exposedGuts) {
+ mGutsManager.setExposedGuts(entry.row.getGuts());
+ mGutsManager.bindGuts(entry.row);
+ }
+ }
+ }
+
+ private void updateNotification(NotificationData.Entry entry, PackageManager pmUser,
+ StatusBarNotification sbn, ExpandableNotificationRow row) {
+ row.setNeedsRedaction(mLockscreenUserManager.needsRedaction(entry));
+ boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey());
+ boolean isUpdate = mNotificationData.get(entry.key) != null;
+ boolean wasLowPriority = row.isLowPriority();
+ row.setIsLowPriority(isLowPriority);
+ row.setLowPriorityStateUpdated(isUpdate && (wasLowPriority != isLowPriority));
+ // bind the click event to the content area
+ mNotificationClicker.register(row, sbn);
+
+ // Extract target SDK version.
+ try {
+ ApplicationInfo info = pmUser.getApplicationInfo(sbn.getPackageName(), 0);
+ entry.targetSdk = info.targetSdkVersion;
+ } catch (PackageManager.NameNotFoundException ex) {
+ Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex);
+ }
+ row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD
+ && entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
+ entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
+ entry.autoRedacted = entry.notification.getNotification().publicVersion == null;
+
+ entry.row = row;
+ entry.row.setOnActivatedListener(mPresenter);
+
+ boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(sbn,
+ mNotificationData.getImportance(sbn.getKey()));
+ boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight
+ && !mPresenter.isPresenterFullyCollapsed();
+ row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
+ row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
+ row.updateNotification(entry);
+ }
+
+
+ protected void addNotificationViews(NotificationData.Entry entry) {
+ if (entry == null) {
+ return;
+ }
+ // Add the expanded view and icon.
+ mNotificationData.add(entry);
+ updateNotifications();
+ }
+
+ protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn)
+ throws InflationException {
+ if (DEBUG) {
+ Log.d(TAG, "createNotificationViews(notification=" + sbn);
+ }
+ NotificationData.Entry entry = new NotificationData.Entry(sbn);
+ Dependency.get(LeakDetector.class).trackInstance(entry);
+ entry.createIcons(mContext, sbn);
+ // Construct the expanded view.
+ inflateViews(entry, mListContainer.getViewParentForNotification(entry));
+ return entry;
+ }
+
+ private void addNotificationInternal(StatusBarNotification notification,
+ NotificationListenerService.RankingMap ranking) throws InflationException {
+ String key = notification.getKey();
+ if (DEBUG) Log.d(TAG, "addNotification key=" + key);
+
+ mNotificationData.updateRanking(ranking);
+ NotificationData.Entry shadeEntry = createNotificationViews(notification);
+ boolean isHeadsUped = shouldPeek(shadeEntry);
+ if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) {
+ if (shouldSuppressFullScreenIntent(key)) {
+ if (DEBUG) {
+ Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + key);
+ }
+ } else if (mNotificationData.getImportance(key)
+ < NotificationManager.IMPORTANCE_HIGH) {
+ if (DEBUG) {
+ Log.d(TAG, "No Fullscreen intent: not important enough: "
+ + key);
+ }
+ } else {
+ // Stop screensaver if the notification has a fullscreen intent.
+ // (like an incoming phone call)
+ SystemServicesProxy.getInstance(mContext).awakenDreamsAsync();
+
+ // not immersive & a fullscreen alert should be shown
+ if (DEBUG)
+ Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent");
+ try {
+ EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION,
+ key);
+ notification.getNotification().fullScreenIntent.send();
+ shadeEntry.notifyFullScreenIntentLaunched();
+ mMetricsLogger.count("note_fullscreen", 1);
+ } catch (PendingIntent.CanceledException e) {
+ }
+ }
+ }
+ abortExistingInflation(key);
+
+ mForegroundServiceController.addNotification(notification,
+ mNotificationData.getImportance(key));
+
+ mPendingNotifications.put(key, shadeEntry);
+ }
+
+ @Override
+ public void addNotification(StatusBarNotification notification,
+ NotificationListenerService.RankingMap ranking) {
+ try {
+ addNotificationInternal(notification, ranking);
+ } catch (InflationException e) {
+ handleInflationException(notification, e);
+ }
+ }
+
+ private boolean alertAgain(NotificationData.Entry oldEntry, Notification newNotification) {
+ return oldEntry == null || !oldEntry.hasInterrupted()
+ || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0;
+ }
+
+ private void updateNotificationInternal(StatusBarNotification notification,
+ NotificationListenerService.RankingMap ranking) throws InflationException {
+ if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
+
+ final String key = notification.getKey();
+ abortExistingInflation(key);
+ NotificationData.Entry entry = mNotificationData.get(key);
+ if (entry == null) {
+ return;
+ }
+ mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
+ mRemoteInputManager.onUpdateNotification(entry);
+
+ if (key.equals(mGutsManager.getKeyToRemoveOnGutsClosed())) {
+ mGutsManager.setKeyToRemoveOnGutsClosed(null);
+ Log.w(TAG, "Notification that was kept for guts was updated. " + key);
+ }
+
+ Notification n = notification.getNotification();
+ mNotificationData.updateRanking(ranking);
+
+ final StatusBarNotification oldNotification = entry.notification;
+ entry.notification = notification;
+ mGroupManager.onEntryUpdated(entry, oldNotification);
+
+ entry.updateIcons(mContext, notification);
+ inflateViews(entry, mListContainer.getViewParentForNotification(entry));
+
+ mForegroundServiceController.updateNotification(notification,
+ mNotificationData.getImportance(key));
+
+ boolean shouldPeek = shouldPeek(entry, notification);
+ boolean alertAgain = alertAgain(entry, n);
+
+ updateHeadsUp(key, entry, shouldPeek, alertAgain);
+ updateNotifications();
+
+ if (!notification.isClearable()) {
+ // The user may have performed a dismiss action on the notification, since it's
+ // not clearable we should snap it back.
+ mListContainer.snapViewIfNeeded(entry.row);
+ }
+
+ if (DEBUG) {
+ // Is this for you?
+ boolean isForCurrentUser = mPresenter.isNotificationForCurrentProfiles(notification);
+ Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
+ }
+
+ mCallback.onNotificationUpdated(notification);
+ }
+
+ @Override
+ public void updateNotification(StatusBarNotification notification,
+ NotificationListenerService.RankingMap ranking) {
+ try {
+ updateNotificationInternal(notification, ranking);
+ } catch (InflationException e) {
+ handleInflationException(notification, e);
+ }
+ }
+
+ public void updateNotifications() {
+ mNotificationData.filterAndSort();
+
+ mPresenter.updateNotificationViews();
+ }
+
+ public void updateNotificationRanking(NotificationListenerService.RankingMap ranking) {
+ mNotificationData.updateRanking(ranking);
+ updateNotifications();
+ }
+
+ protected boolean shouldPeek(NotificationData.Entry entry) {
+ return shouldPeek(entry, entry.notification);
+ }
+
+ public boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn) {
+ if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) {
+ if (DEBUG) Log.d(TAG, "No peeking: no huns or vr mode");
+ return false;
+ }
+
+ if (mNotificationData.shouldFilterOut(sbn)) {
+ if (DEBUG) Log.d(TAG, "No peeking: filtered notification: " + sbn.getKey());
+ return false;
+ }
+
+ boolean inUse = mPowerManager.isScreenOn() && !mSystemServicesProxy.isDreaming();
+
+ if (!inUse && !mPresenter.isDozing()) {
+ if (DEBUG) {
+ Log.d(TAG, "No peeking: not in use: " + sbn.getKey());
+ }
+ return false;
+ }
+
+ if (!mPresenter.isDozing() && mNotificationData.shouldSuppressScreenOn(sbn.getKey())) {
+ if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
+ return false;
+ }
+
+ if (mPresenter.isDozing() && mNotificationData.shouldSuppressScreenOff(sbn.getKey())) {
+ if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
+ return false;
+ }
+
+ if (entry.hasJustLaunchedFullScreenIntent()) {
+ if (DEBUG) Log.d(TAG, "No peeking: recent fullscreen: " + sbn.getKey());
+ return false;
+ }
+
+ if (isSnoozedPackage(sbn)) {
+ if (DEBUG) Log.d(TAG, "No peeking: snoozed package: " + sbn.getKey());
+ return false;
+ }
+
+ // Allow peeking for DEFAULT notifications only if we're on Ambient Display.
+ int importanceLevel = mPresenter.isDozing() ? NotificationManager.IMPORTANCE_DEFAULT
+ : NotificationManager.IMPORTANCE_HIGH;
+ if (mNotificationData.getImportance(sbn.getKey()) < importanceLevel) {
+ if (DEBUG) Log.d(TAG, "No peeking: unimportant notification: " + sbn.getKey());
+ return false;
+ }
+
+ // Don't peek notifications that are suppressed due to group alert behavior
+ if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
+ if (DEBUG) Log.d(TAG, "No peeking: suppressed due to group alert behavior");
+ return false;
+ }
+
+ if (!mCallback.shouldPeek(entry, sbn)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected void setNotificationShown(StatusBarNotification n) {
+ setNotificationsShown(new String[]{n.getKey()});
+ }
+
+ protected void setNotificationsShown(String[] keys) {
+ try {
+ mNotificationListener.setNotificationsShown(keys);
+ } catch (RuntimeException e) {
+ Log.d(TAG, "failed setNotificationsShown: ", e);
+ }
+ }
+
+ protected boolean isSnoozedPackage(StatusBarNotification sbn) {
+ return mHeadsUpManager.isSnoozed(sbn.getPackageName());
+ }
+
+ protected void updateHeadsUp(String key, NotificationData.Entry entry, boolean shouldPeek,
+ boolean alertAgain) {
+ final boolean wasHeadsUp = isHeadsUp(key);
+ if (wasHeadsUp) {
+ if (!shouldPeek) {
+ // We don't want this to be interrupting anymore, lets remove it
+ mHeadsUpManager.removeNotification(key, false /* ignoreEarliestRemovalTime */);
+ } else {
+ mHeadsUpManager.updateNotification(entry, alertAgain);
+ }
+ } else if (shouldPeek && alertAgain) {
+ // This notification was updated to be a heads-up, show it!
+ mHeadsUpManager.showNotification(entry);
+ }
+ }
+
+ protected boolean isHeadsUp(String key) {
+ return mHeadsUpManager.isHeadsUp(key);
+ }
+
+ /**
+ * Callback for NotificationEntryManager.
+ */
+ public interface Callback {
+
+ /**
+ * Called when a new entry is created.
+ *
+ * @param shadeEntry entry that was created
+ */
+ void onNotificationAdded(NotificationData.Entry shadeEntry);
+
+ /**
+ * Called when a notification was updated.
+ *
+ * @param notification notification that was updated
+ */
+ void onNotificationUpdated(StatusBarNotification notification);
+
+ /**
+ * Called when a notification was removed.
+ *
+ * @param key key of notification that was removed
+ * @param old StatusBarNotification of the notification before it was removed
+ */
+ void onNotificationRemoved(String key, StatusBarNotification old);
+
+
+ /**
+ * Called when a notification is clicked.
+ *
+ * @param sbn notification that was clicked
+ * @param row row for that notification
+ */
+ void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row);
+
+ /**
+ * Called when a new notification and row is created.
+ *
+ * @param entry entry for the notification
+ * @param pmUser package manager for user
+ * @param sbn notification
+ * @param row row for the notification
+ */
+ void onBindRow(NotificationData.Entry entry, PackageManager pmUser,
+ StatusBarNotification sbn, ExpandableNotificationRow row);
+
+ /**
+ * Removes a notification immediately.
+ *
+ * @param statusBarNotification notification that is being removed
+ */
+ void onPerformRemoveNotification(StatusBarNotification statusBarNotification);
+
+ /**
+ * Returns true if NotificationEntryManager should peek this notification.
+ *
+ * @param entry entry of the notification that might be peeked
+ * @param sbn notification that might be peeked
+ * @return true if the notification should be peeked
+ */
+ boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn);
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationGutsManager.java b/com/android/systemui/statusbar/NotificationGutsManager.java
index f451fda6..87ad6f6b 100644
--- a/com/android/systemui/statusbar/NotificationGutsManager.java
+++ b/com/android/systemui/statusbar/NotificationGutsManager.java
@@ -37,13 +37,11 @@ import android.view.accessibility.AccessibilityManager;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
-import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dependency;
import com.android.systemui.Dumpable;
import com.android.systemui.Interpolators;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.stack.StackStateAnimator;
import java.io.FileDescriptor;
@@ -66,30 +64,25 @@ public class NotificationGutsManager implements Dumpable {
private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
private final Set<String> mNonBlockablePkgs;
- private final NotificationPresenter mPresenter;
- // TODO: Create NotificationListContainer interface and use it instead of
- // NotificationStackScrollLayout here
- private final NotificationStackScrollLayout mStackScroller;
private final Context mContext;
private final AccessibilityManager mAccessibilityManager;
+
+ // Dependencies:
+ private final NotificationLockscreenUserManager mLockscreenUserManager =
+ Dependency.get(NotificationLockscreenUserManager.class);
+
// which notification is currently being longpress-examined by the user
private NotificationGuts mNotificationGutsExposed;
private NotificationMenuRowPlugin.MenuItem mGutsMenuItem;
- private final NotificationInfo.CheckSaveListener mCheckSaveListener;
- private final OnSettingsClickListener mOnSettingsClickListener;
+ protected NotificationPresenter mPresenter;
+ protected NotificationEntryManager mEntryManager;
+ private NotificationListContainer mListContainer;
+ private NotificationInfo.CheckSaveListener mCheckSaveListener;
+ private OnSettingsClickListener mOnSettingsClickListener;
private String mKeyToRemoveOnGutsClosed;
- public NotificationGutsManager(
- NotificationPresenter presenter,
- NotificationStackScrollLayout stackScroller,
- NotificationInfo.CheckSaveListener checkSaveListener,
- Context context,
- OnSettingsClickListener onSettingsClickListener) {
- mPresenter = presenter;
- mStackScroller = stackScroller;
- mCheckSaveListener = checkSaveListener;
+ public NotificationGutsManager(Context context) {
mContext = context;
- mOnSettingsClickListener = onSettingsClickListener;
Resources res = context.getResources();
mNonBlockablePkgs = new HashSet<>();
@@ -100,6 +93,17 @@ public class NotificationGutsManager implements Dumpable {
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
+ public void setUpWithPresenter(NotificationPresenter presenter,
+ NotificationEntryManager entryManager, NotificationListContainer listContainer,
+ NotificationInfo.CheckSaveListener checkSaveListener,
+ OnSettingsClickListener onSettingsClickListener) {
+ mPresenter = presenter;
+ mEntryManager = entryManager;
+ mListContainer = listContainer;
+ mCheckSaveListener = checkSaveListener;
+ mOnSettingsClickListener = onSettingsClickListener;
+ }
+
public String getKeyToRemoveOnGutsClosed() {
return mKeyToRemoveOnGutsClosed;
}
@@ -152,7 +156,7 @@ public class NotificationGutsManager implements Dumpable {
final NotificationGuts guts = row.getGuts();
guts.setClosedListener((NotificationGuts g) -> {
if (!g.willBeRemoved() && !row.isRemoved()) {
- mStackScroller.onHeightChanged(
+ mListContainer.onHeightChanged(
row, !mPresenter.isPresenterFullyCollapsed() /* needsAnimation */);
}
if (mNotificationGutsExposed == g) {
@@ -162,18 +166,18 @@ public class NotificationGutsManager implements Dumpable {
String key = sbn.getKey();
if (key.equals(mKeyToRemoveOnGutsClosed)) {
mKeyToRemoveOnGutsClosed = null;
- mPresenter.removeNotification(key, mPresenter.getLatestRankingMap());
+ mEntryManager.removeNotification(key, mEntryManager.getLatestRankingMap());
}
});
View gutsView = item.getGutsView();
if (gutsView instanceof NotificationSnooze) {
NotificationSnooze snoozeGuts = (NotificationSnooze) gutsView;
- snoozeGuts.setSnoozeListener(mStackScroller.getSwipeActionHelper());
+ snoozeGuts.setSnoozeListener(mListContainer.getSwipeActionHelper());
snoozeGuts.setStatusBarNotification(sbn);
snoozeGuts.setSnoozeOptions(row.getEntry().snoozeCriteria);
guts.setHeightChangedListener((NotificationGuts g) -> {
- mStackScroller.onHeightChanged(row, row.isShown() /* needsAnimation */);
+ mListContainer.onHeightChanged(row, row.isShown() /* needsAnimation */);
});
}
@@ -189,7 +193,7 @@ public class NotificationGutsManager implements Dumpable {
// system user.
NotificationInfo.OnSettingsClickListener onSettingsClick = null;
if (!userHandle.equals(UserHandle.ALL)
- || mPresenter.getCurrentUserId() == UserHandle.USER_SYSTEM) {
+ || mLockscreenUserManager.getCurrentUserId() == UserHandle.USER_SYSTEM) {
onSettingsClick = (View v, NotificationChannel channel, int appUid) -> {
mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_NOTE_INFO);
guts.resetFalsingCheck();
@@ -251,7 +255,7 @@ public class NotificationGutsManager implements Dumpable {
mNotificationGutsExposed.closeControls(removeLeavebehinds, removeControls, x, y, force);
}
if (resetMenu) {
- mStackScroller.resetExposedMenuView(false /* animate */, true /* force */);
+ mListContainer.resetExposedMenuView(false /* animate */, true /* force */);
}
}
@@ -344,7 +348,7 @@ public class NotificationGutsManager implements Dumpable {
!mAccessibilityManager.isTouchExplorationEnabled());
guts.setExposed(true /* exposed */, needsFalsingProtection);
row.closeRemoteInput();
- mStackScroller.onHeightChanged(row, true /* needsAnimation */);
+ mListContainer.onHeightChanged(row, true /* needsAnimation */);
mNotificationGutsExposed = guts;
mGutsMenuItem = item;
}
@@ -354,7 +358,8 @@ public class NotificationGutsManager implements Dumpable {
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- pw.print("mKeyToRemoveOnGutsClosed: ");
+ pw.println("NotificationGutsManager state:");
+ pw.print(" mKeyToRemoveOnGutsClosed: ");
pw.println(mKeyToRemoveOnGutsClosed);
}
diff --git a/com/android/systemui/statusbar/NotificationListContainer.java b/com/android/systemui/statusbar/NotificationListContainer.java
new file mode 100644
index 00000000..43be44de
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationListContainer.java
@@ -0,0 +1,182 @@
+/*
+ * 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 com.android.systemui.statusbar;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+
+/**
+ * Interface representing the entity that contains notifications. It can have
+ * notification views added and removed from it, and will manage displaying them to the user.
+ */
+public interface NotificationListContainer {
+
+ /**
+ * Called when a child is being transferred.
+ *
+ * @param childTransferInProgress whether child transfer is in progress
+ */
+ void setChildTransferInProgress(boolean childTransferInProgress);
+
+ /**
+ * Change the position of child to a new location
+ *
+ * @param child the view to change the position for
+ * @param newIndex the new index
+ */
+ void changeViewPosition(View child, int newIndex);
+
+ /**
+ * Called when a child was added to a group.
+ *
+ * @param row row of the group child that was added
+ */
+ void notifyGroupChildAdded(View row);
+
+ /**
+ * Called when a child was removed from a group.
+ *
+ * @param row row of the child that was removed
+ * @param childrenContainer ViewGroup of the group that the child was removed from
+ */
+ void notifyGroupChildRemoved(View row, ViewGroup childrenContainer);
+
+ /**
+ * Generate an animation for an added child view.
+ *
+ * @param child The view to be added.
+ * @param fromMoreCard Whether this add is coming from the "more" card on lockscreen.
+ */
+ void generateAddAnimation(View child, boolean fromMoreCard);
+
+ /**
+ * Generate a child order changed event.
+ */
+ void generateChildOrderChangedEvent();
+
+ /**
+ * Returns the number of children in the NotificationListContainer.
+ *
+ * @return the number of children in the NotificationListContainer
+ */
+ int getContainerChildCount();
+
+ /**
+ * Gets the ith child in the NotificationListContainer.
+ *
+ * @param i ith child to get
+ * @return the ith child in the list container
+ */
+ View getContainerChildAt(int i);
+
+ /**
+ * Remove a view from the container
+ *
+ * @param v view to remove
+ */
+ void removeContainerView(View v);
+
+ /**
+ * Add a view to the container
+ *
+ * @param v view to add
+ */
+ void addContainerView(View v);
+
+ /**
+ * Sets the maximum number of notifications to display.
+ *
+ * @param maxNotifications max number of notifications to display
+ */
+ void setMaxDisplayedNotifications(int maxNotifications);
+
+ /**
+ * Handle snapping a non-dismissable row back if the user tried to dismiss it.
+ *
+ * @param row row to snap back
+ */
+ void snapViewIfNeeded(ExpandableNotificationRow row);
+
+ /**
+ * Get the view parent for a notification entry. For example, NotificationStackScrollLayout.
+ *
+ * @param entry entry to get the view parent for
+ * @return the view parent for entry
+ */
+ ViewGroup getViewParentForNotification(NotificationData.Entry entry);
+
+ /**
+ * Called when the height of an expandable view changes.
+ *
+ * @param view view whose height changed
+ * @param animate whether this change should be animated
+ */
+ void onHeightChanged(ExpandableView view, boolean animate);
+
+ /**
+ * Resets the currently exposed menu view.
+ *
+ * @param animate whether to animate the closing/change of menu view
+ * @param force reset the menu view even if it looks like it is already reset
+ */
+ void resetExposedMenuView(boolean animate, boolean force);
+
+ /**
+ * Returns the NotificationSwipeActionHelper for the NotificationListContainer.
+ *
+ * @return swipe action helper for the list container
+ */
+ NotificationSwipeActionHelper getSwipeActionHelper();
+
+ /**
+ * Called when a notification is removed from the shade. This cleans up the state for a
+ * given view.
+ *
+ * @param view view to clean up view state for
+ */
+ void cleanUpViewState(View view);
+
+ /**
+ * Returns whether an ExpandableNotificationRow is in a visible location or not.
+ *
+ * @param row
+ * @return true if row is in a visible location
+ */
+ boolean isInVisibleLocation(ExpandableNotificationRow row);
+
+ /**
+ * Sets a listener to listen for changes in notification locations.
+ *
+ * @param listener listener to set
+ */
+ void setChildLocationsChangedListener(
+ NotificationLogger.OnChildLocationsChangedListener listener);
+
+ /**
+ * Called when an update to the notification view hierarchy is completed.
+ */
+ default void onNotificationViewUpdateFinished() {}
+
+ /**
+ * Returns true if there are pulsing notifications.
+ *
+ * @return true if has pulsing notifications
+ */
+ boolean hasPulsingNotifications();
+}
diff --git a/com/android/systemui/statusbar/NotificationListener.java b/com/android/systemui/statusbar/NotificationListener.java
new file mode 100644
index 00000000..0144f424
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationListener.java
@@ -0,0 +1,145 @@
+/*
+ * 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 com.android.systemui.statusbar;
+
+import static com.android.systemui.statusbar.RemoteInputController.processForRemoteInput;
+import static com.android.systemui.statusbar.phone.StatusBar.DEBUG;
+import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_CHILD_NOTIFICATIONS;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins;
+
+/**
+ * This class handles listening to notification updates and passing them along to
+ * NotificationPresenter to be displayed to the user.
+ */
+public class NotificationListener extends NotificationListenerWithPlugins {
+ private static final String TAG = "NotificationListener";
+
+ // Dependencies:
+ private final NotificationRemoteInputManager mRemoteInputManager =
+ Dependency.get(NotificationRemoteInputManager.class);
+
+ private final Context mContext;
+
+ protected NotificationPresenter mPresenter;
+ protected NotificationEntryManager mEntryManager;
+
+ public NotificationListener(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void onListenerConnected() {
+ if (DEBUG) Log.d(TAG, "onListenerConnected");
+ onPluginConnected();
+ final StatusBarNotification[] notifications = getActiveNotifications();
+ if (notifications == null) {
+ Log.w(TAG, "onListenerConnected unable to get active notifications.");
+ return;
+ }
+ final RankingMap currentRanking = getCurrentRanking();
+ mPresenter.getHandler().post(() -> {
+ for (StatusBarNotification sbn : notifications) {
+ mEntryManager.addNotification(sbn, currentRanking);
+ }
+ });
+ }
+
+ @Override
+ public void onNotificationPosted(final StatusBarNotification sbn,
+ final RankingMap rankingMap) {
+ if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn);
+ if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
+ mPresenter.getHandler().post(() -> {
+ processForRemoteInput(sbn.getNotification(), mContext);
+ String key = sbn.getKey();
+ mRemoteInputManager.getKeysKeptForRemoteInput().remove(key);
+ boolean isUpdate =
+ mEntryManager.getNotificationData().get(key) != null;
+ // In case we don't allow child notifications, we ignore children of
+ // notifications that have a summary, since` we're not going to show them
+ // anyway. This is true also when the summary is canceled,
+ // because children are automatically canceled by NoMan in that case.
+ if (!ENABLE_CHILD_NOTIFICATIONS
+ && mPresenter.getGroupManager().isChildInGroupWithSummary(sbn)) {
+ if (DEBUG) {
+ Log.d(TAG, "Ignoring group child due to existing summary: " + sbn);
+ }
+
+ // Remove existing notification to avoid stale data.
+ if (isUpdate) {
+ mEntryManager.removeNotification(key, rankingMap);
+ } else {
+ mEntryManager.getNotificationData()
+ .updateRanking(rankingMap);
+ }
+ return;
+ }
+ if (isUpdate) {
+ mEntryManager.updateNotification(sbn, rankingMap);
+ } else {
+ mEntryManager.addNotification(sbn, rankingMap);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification sbn,
+ final RankingMap rankingMap) {
+ if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn);
+ if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) {
+ final String key = sbn.getKey();
+ mPresenter.getHandler().post(() -> {
+ mEntryManager.removeNotification(key, rankingMap);
+ });
+ }
+ }
+
+ @Override
+ public void onNotificationRankingUpdate(final RankingMap rankingMap) {
+ if (DEBUG) Log.d(TAG, "onRankingUpdate");
+ if (rankingMap != null) {
+ RankingMap r = onPluginRankingUpdate(rankingMap);
+ mPresenter.getHandler().post(() -> {
+ mEntryManager.updateNotificationRanking(r);
+ });
+ }
+ }
+
+ public void setUpWithPresenter(NotificationPresenter presenter,
+ NotificationEntryManager entryManager) {
+ mPresenter = presenter;
+ mEntryManager = entryManager;
+
+ try {
+ registerAsSystemService(mContext,
+ new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()),
+ UserHandle.USER_ALL);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to register notification listener", e);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
new file mode 100644
index 00000000..bcdc269f
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
@@ -0,0 +1,457 @@
+/*
+ * 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 com.android.systemui.statusbar;
+
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.OverviewProxyService;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Handles keeping track of the current user, profiles, and various things related to hiding
+ * contents, redacting notifications, and the lockscreen.
+ */
+public class NotificationLockscreenUserManager implements Dumpable {
+ private static final String TAG = "LockscreenUserManager";
+ private static final boolean ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT = false;
+ public static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+ public static final String NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION
+ = "com.android.systemui.statusbar.work_challenge_unlocked_notification_action";
+
+ private final DevicePolicyManager mDevicePolicyManager;
+ private final SparseBooleanArray mLockscreenPublicMode = new SparseBooleanArray();
+ private final SparseBooleanArray mUsersAllowingPrivateNotifications = new SparseBooleanArray();
+ private final SparseBooleanArray mUsersAllowingNotifications = new SparseBooleanArray();
+ private final DeviceProvisionedController mDeviceProvisionedController =
+ Dependency.get(DeviceProvisionedController.class);
+ private final UserManager mUserManager;
+ private final IStatusBarService mBarService;
+
+ private boolean mShowLockscreenNotifications;
+ private boolean mAllowLockscreenRemoteInput;
+
+ protected final BroadcastReceiver mAllUsersReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+
+ if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(action) &&
+ isCurrentProfile(getSendingUserId())) {
+ mUsersAllowingPrivateNotifications.clear();
+ updateLockscreenNotificationSetting();
+ mEntryManager.updateNotifications();
+ } else if (Intent.ACTION_DEVICE_LOCKED_CHANGED.equals(action)) {
+ if (userId != mCurrentUserId && isCurrentProfile(userId)) {
+ mPresenter.onWorkChallengeChanged();
+ }
+ }
+ }
+ };
+
+ protected final BroadcastReceiver mBaseBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+ mCurrentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ updateCurrentProfilesCache();
+ Log.v(TAG, "userId " + mCurrentUserId + " is in the house");
+
+ updateLockscreenNotificationSetting();
+
+ mPresenter.onUserSwitched(mCurrentUserId);
+ } else if (Intent.ACTION_USER_ADDED.equals(action)) {
+ updateCurrentProfilesCache();
+ } else if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
+ // Start the overview connection to the launcher service
+ Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser();
+ } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
+ try {
+ final int lastResumedActivityUserId =
+ ActivityManager.getService().getLastResumedActivityUserId();
+ if (mUserManager.isManagedProfile(lastResumedActivityUserId)) {
+ showForegroundManagedProfileActivityToast();
+ }
+ } catch (RemoteException e) {
+ // Abandon hope activity manager not running.
+ }
+ } else if (NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION.equals(action)) {
+ final IntentSender intentSender = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+ final String notificationKey = intent.getStringExtra(Intent.EXTRA_INDEX);
+ if (intentSender != null) {
+ try {
+ mContext.startIntentSender(intentSender, null, 0, 0, 0);
+ } catch (IntentSender.SendIntentException e) {
+ /* ignore */
+ }
+ }
+ if (notificationKey != null) {
+ try {
+ mBarService.onNotificationClick(notificationKey);
+ } catch (RemoteException e) {
+ /* ignore */
+ }
+ }
+ }
+ }
+ };
+
+ protected final Context mContext;
+ protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>();
+
+ protected int mCurrentUserId = 0;
+ protected NotificationPresenter mPresenter;
+ protected NotificationEntryManager mEntryManager;
+ protected ContentObserver mLockscreenSettingsObserver;
+ protected ContentObserver mSettingsObserver;
+
+ public NotificationLockscreenUserManager(Context context) {
+ mContext = context;
+ mDevicePolicyManager = (DevicePolicyManager) mContext.getSystemService(
+ Context.DEVICE_POLICY_SERVICE);
+ mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mCurrentUserId = ActivityManager.getCurrentUser();
+ mBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ }
+
+ public void setUpWithPresenter(NotificationPresenter presenter,
+ NotificationEntryManager entryManager) {
+ mPresenter = presenter;
+ mEntryManager = entryManager;
+
+ mLockscreenSettingsObserver = new ContentObserver(mPresenter.getHandler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ // We don't know which user changed LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS or
+ // LOCK_SCREEN_SHOW_NOTIFICATIONS, so we just dump our cache ...
+ mUsersAllowingPrivateNotifications.clear();
+ mUsersAllowingNotifications.clear();
+ // ... and refresh all the notifications
+ updateLockscreenNotificationSetting();
+ mEntryManager.updateNotifications();
+ }
+ };
+
+ mSettingsObserver = new ContentObserver(mPresenter.getHandler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ updateLockscreenNotificationSetting();
+ if (mDeviceProvisionedController.isDeviceProvisioned()) {
+ mEntryManager.updateNotifications();
+ }
+ }
+ };
+
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS), false,
+ mLockscreenSettingsObserver,
+ UserHandle.USER_ALL);
+
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS),
+ true,
+ mLockscreenSettingsObserver,
+ UserHandle.USER_ALL);
+
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false,
+ mSettingsObserver);
+
+ if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT),
+ false,
+ mSettingsObserver,
+ UserHandle.USER_ALL);
+ }
+
+ IntentFilter allUsersFilter = new IntentFilter();
+ allUsersFilter.addAction(
+ DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
+ allUsersFilter.addAction(Intent.ACTION_DEVICE_LOCKED_CHANGED);
+ mContext.registerReceiverAsUser(mAllUsersReceiver, UserHandle.ALL, allUsersFilter,
+ null, null);
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ filter.addAction(Intent.ACTION_USER_ADDED);
+ filter.addAction(Intent.ACTION_USER_PRESENT);
+ filter.addAction(Intent.ACTION_USER_UNLOCKED);
+ mContext.registerReceiver(mBaseBroadcastReceiver, filter);
+
+ IntentFilter internalFilter = new IntentFilter();
+ internalFilter.addAction(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
+ mContext.registerReceiver(mBaseBroadcastReceiver, internalFilter, PERMISSION_SELF, null);
+
+ updateCurrentProfilesCache();
+
+ mSettingsObserver.onChange(false); // set up
+ }
+
+ private void showForegroundManagedProfileActivityToast() {
+ Toast toast = Toast.makeText(mContext,
+ R.string.managed_profile_foreground_toast,
+ Toast.LENGTH_SHORT);
+ TextView text = toast.getView().findViewById(android.R.id.message);
+ text.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ R.drawable.stat_sys_managed_profile_status, 0, 0, 0);
+ int paddingPx = mContext.getResources().getDimensionPixelSize(
+ R.dimen.managed_profile_toast_padding);
+ text.setCompoundDrawablePadding(paddingPx);
+ toast.show();
+ }
+
+ public boolean shouldShowLockscreenNotifications() {
+ return mShowLockscreenNotifications;
+ }
+
+ public boolean shouldAllowLockscreenRemoteInput() {
+ return mAllowLockscreenRemoteInput;
+ }
+
+ public boolean isCurrentProfile(int userId) {
+ synchronized (mCurrentProfiles) {
+ return userId == UserHandle.USER_ALL || mCurrentProfiles.get(userId) != null;
+ }
+ }
+
+ /**
+ * Returns true if we're on a secure lockscreen and the user wants to hide notification data.
+ * If so, notifications should be hidden.
+ */
+ public boolean shouldHideNotifications(int userId) {
+ return isLockscreenPublicMode(userId) && !userAllowsNotificationsInPublic(userId)
+ || (userId != mCurrentUserId && shouldHideNotifications(mCurrentUserId));
+ }
+
+ /**
+ * Returns true if we're on a secure lockscreen and the user wants to hide notifications via
+ * package-specific override.
+ */
+ public boolean shouldHideNotifications(String key) {
+ return isLockscreenPublicMode(mCurrentUserId)
+ && mEntryManager.getNotificationData().getVisibilityOverride(key) ==
+ Notification.VISIBILITY_SECRET;
+ }
+
+ public boolean shouldShowOnKeyguard(StatusBarNotification sbn) {
+ return mShowLockscreenNotifications
+ && !mEntryManager.getNotificationData().isAmbient(sbn.getKey());
+ }
+
+ private void setShowLockscreenNotifications(boolean show) {
+ mShowLockscreenNotifications = show;
+ }
+
+ private void setLockscreenAllowRemoteInput(boolean allowLockscreenRemoteInput) {
+ mAllowLockscreenRemoteInput = allowLockscreenRemoteInput;
+ }
+
+ protected void updateLockscreenNotificationSetting() {
+ final boolean show = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
+ 1,
+ mCurrentUserId) != 0;
+ final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(
+ null /* admin */, mCurrentUserId);
+ final boolean allowedByDpm = (dpmFlags
+ & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0;
+
+ setShowLockscreenNotifications(show && allowedByDpm);
+
+ if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
+ final boolean remoteInput = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT,
+ 0,
+ mCurrentUserId) != 0;
+ final boolean remoteInputDpm =
+ (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_REMOTE_INPUT) == 0;
+
+ setLockscreenAllowRemoteInput(remoteInput && remoteInputDpm);
+ } else {
+ setLockscreenAllowRemoteInput(false);
+ }
+ }
+
+ /**
+ * Has the given user chosen to allow their private (full) notifications to be shown even
+ * when the lockscreen is in "public" (secure & locked) mode?
+ */
+ public boolean userAllowsPrivateNotificationsInPublic(int userHandle) {
+ if (userHandle == UserHandle.USER_ALL) {
+ return true;
+ }
+
+ if (mUsersAllowingPrivateNotifications.indexOfKey(userHandle) < 0) {
+ final boolean allowedByUser = 0 != Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle);
+ final boolean allowedByDpm = adminAllowsUnredactedNotifications(userHandle);
+ final boolean allowed = allowedByUser && allowedByDpm;
+ mUsersAllowingPrivateNotifications.append(userHandle, allowed);
+ return allowed;
+ }
+
+ return mUsersAllowingPrivateNotifications.get(userHandle);
+ }
+
+ private boolean adminAllowsUnredactedNotifications(int userHandle) {
+ if (userHandle == UserHandle.USER_ALL) {
+ return true;
+ }
+ final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */,
+ userHandle);
+ return (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS) == 0;
+ }
+
+ /**
+ * Save the current "public" (locked and secure) state of the lockscreen.
+ */
+ public void setLockscreenPublicMode(boolean publicMode, int userId) {
+ mLockscreenPublicMode.put(userId, publicMode);
+ }
+
+ public boolean isLockscreenPublicMode(int userId) {
+ if (userId == UserHandle.USER_ALL) {
+ return mLockscreenPublicMode.get(mCurrentUserId, false);
+ }
+ return mLockscreenPublicMode.get(userId, false);
+ }
+
+ /**
+ * Has the given user chosen to allow notifications to be shown even when the lockscreen is in
+ * "public" (secure & locked) mode?
+ */
+ private boolean userAllowsNotificationsInPublic(int userHandle) {
+ if (userHandle == UserHandle.USER_ALL) {
+ return true;
+ }
+
+ if (mUsersAllowingNotifications.indexOfKey(userHandle) < 0) {
+ final boolean allowed = 0 != Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, userHandle);
+ mUsersAllowingNotifications.append(userHandle, allowed);
+ return allowed;
+ }
+
+ return mUsersAllowingNotifications.get(userHandle);
+ }
+
+ /** @return true if the entry needs redaction when on the lockscreen. */
+ public boolean needsRedaction(NotificationData.Entry ent) {
+ int userId = ent.notification.getUserId();
+
+ boolean currentUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
+ boolean notiUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(userId);
+ boolean redactedLockscreen = currentUserWantsRedaction || notiUserWantsRedaction;
+
+ boolean notificationRequestsRedaction =
+ ent.notification.getNotification().visibility == Notification.VISIBILITY_PRIVATE;
+ boolean userForcesRedaction = packageHasVisibilityOverride(ent.notification.getKey());
+
+ return userForcesRedaction || notificationRequestsRedaction && redactedLockscreen;
+ }
+
+ private boolean packageHasVisibilityOverride(String key) {
+ return mEntryManager.getNotificationData().getVisibilityOverride(key) ==
+ Notification.VISIBILITY_PRIVATE;
+ }
+
+
+ private void updateCurrentProfilesCache() {
+ synchronized (mCurrentProfiles) {
+ mCurrentProfiles.clear();
+ if (mUserManager != null) {
+ for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) {
+ mCurrentProfiles.put(user.id, user);
+ }
+ }
+ }
+ }
+
+ public boolean isAnyProfilePublicMode() {
+ for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) {
+ if (isLockscreenPublicMode(mCurrentProfiles.valueAt(i).id)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the current user id. This can change if the user is switched.
+ */
+ public int getCurrentUserId() {
+ return mCurrentUserId;
+ }
+
+ public SparseArray<UserInfo> getCurrentProfiles() {
+ return mCurrentProfiles;
+ }
+
+ public void destroy() {
+ mContext.unregisterReceiver(mBaseBroadcastReceiver);
+ mContext.unregisterReceiver(mAllUsersReceiver);
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("NotificationLockscreenUserManager state:");
+ pw.print(" mCurrentUserId=");
+ pw.println(mCurrentUserId);
+ pw.print(" mShowLockscreenNotifications=");
+ pw.println(mShowLockscreenNotifications);
+ pw.print(" mAllowLockscreenRemoteInput=");
+ pw.println(mAllowLockscreenRemoteInput);
+ pw.print(" mCurrentProfiles=");
+ for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) {
+ final int userId = mCurrentProfiles.valueAt(i).id;
+ pw.print("" + userId + " ");
+ }
+ pw.println();
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationLogger.java b/com/android/systemui/statusbar/NotificationLogger.java
new file mode 100644
index 00000000..4225f83c
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationLogger.java
@@ -0,0 +1,227 @@
+/*
+ * 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 com.android.systemui.statusbar;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.service.notification.NotificationListenerService;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.Dependency;
+import com.android.systemui.UiOffloadThread;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Handles notification logging, in particular, logging which notifications are visible and which
+ * are not.
+ */
+public class NotificationLogger {
+ private static final String TAG = "NotificationLogger";
+
+ /** The minimum delay in ms between reports of notification visibility. */
+ private static final int VISIBILITY_REPORT_MIN_DELAY_MS = 500;
+
+ /** Keys of notifications currently visible to the user. */
+ private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications =
+ new ArraySet<>();
+
+ // Dependencies:
+ private final NotificationListenerService mNotificationListener =
+ Dependency.get(NotificationListener.class);
+ private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
+
+ protected NotificationEntryManager mEntryManager;
+ protected Handler mHandler = new Handler();
+ protected IStatusBarService mBarService;
+ private long mLastVisibilityReportUptimeMs;
+ private NotificationListContainer mListContainer;
+
+ protected final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
+ new OnChildLocationsChangedListener() {
+ @Override
+ public void onChildLocationsChanged() {
+ if (mHandler.hasCallbacks(mVisibilityReporter)) {
+ // Visibilities will be reported when the existing
+ // callback is executed.
+ return;
+ }
+ // Calculate when we're allowed to run the visibility
+ // reporter. Note that this timestamp might already have
+ // passed. That's OK, the callback will just be executed
+ // ASAP.
+ long nextReportUptimeMs =
+ mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
+ mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
+ }
+ };
+
+ // Tracks notifications currently visible in mNotificationStackScroller and
+ // emits visibility events via NoMan on changes.
+ protected final Runnable mVisibilityReporter = new Runnable() {
+ private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications =
+ new ArraySet<>();
+ private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications =
+ new ArraySet<>();
+ private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications =
+ new ArraySet<>();
+
+ @Override
+ public void run() {
+ mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis();
+
+ // 1. Loop over mNotificationData entries:
+ // A. Keep list of visible notifications.
+ // B. Keep list of previously hidden, now visible notifications.
+ // 2. Compute no-longer visible notifications by removing currently
+ // visible notifications from the set of previously visible
+ // notifications.
+ // 3. Report newly visible and no-longer visible notifications.
+ // 4. Keep currently visible notifications for next report.
+ ArrayList<NotificationData.Entry> activeNotifications = mEntryManager
+ .getNotificationData().getActiveNotifications();
+ int N = activeNotifications.size();
+ for (int i = 0; i < N; i++) {
+ NotificationData.Entry entry = activeNotifications.get(i);
+ String key = entry.notification.getKey();
+ boolean isVisible = mListContainer.isInVisibleLocation(entry.row);
+ NotificationVisibility visObj = NotificationVisibility.obtain(key, i, isVisible);
+ boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
+ if (isVisible) {
+ // Build new set of visible notifications.
+ mTmpCurrentlyVisibleNotifications.add(visObj);
+ if (!previouslyVisible) {
+ mTmpNewlyVisibleNotifications.add(visObj);
+ }
+ } else {
+ // release object
+ visObj.recycle();
+ }
+ }
+ mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications);
+ mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications);
+
+ logNotificationVisibilityChanges(
+ mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications);
+
+ recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
+ mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications);
+
+ recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
+ mTmpCurrentlyVisibleNotifications.clear();
+ mTmpNewlyVisibleNotifications.clear();
+ mTmpNoLongerVisibleNotifications.clear();
+ }
+ };
+
+ public NotificationLogger() {
+ mBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ }
+
+ public void setUpWithEntryManager(NotificationEntryManager entryManager,
+ NotificationListContainer listContainer) {
+ mEntryManager = entryManager;
+ mListContainer = listContainer;
+ }
+
+ public void stopNotificationLogging() {
+ // Report all notifications as invisible and turn down the
+ // reporter.
+ if (!mCurrentlyVisibleNotifications.isEmpty()) {
+ logNotificationVisibilityChanges(
+ Collections.emptyList(), mCurrentlyVisibleNotifications);
+ recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
+ }
+ mHandler.removeCallbacks(mVisibilityReporter);
+ mListContainer.setChildLocationsChangedListener(null);
+ }
+
+ public void startNotificationLogging() {
+ mListContainer.setChildLocationsChangedListener(mNotificationLocationsChangedListener);
+ // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't
+ // cause the scroller to emit child location events. Hence generate
+ // one ourselves to guarantee that we're reporting visible
+ // notifications.
+ // (Note that in cases where the scroller does emit events, this
+ // additional event doesn't break anything.)
+ mNotificationLocationsChangedListener.onChildLocationsChanged();
+ }
+
+ private void logNotificationVisibilityChanges(
+ Collection<NotificationVisibility> newlyVisible,
+ Collection<NotificationVisibility> noLongerVisible) {
+ if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
+ return;
+ }
+ NotificationVisibility[] newlyVisibleAr =
+ newlyVisible.toArray(new NotificationVisibility[newlyVisible.size()]);
+ NotificationVisibility[] noLongerVisibleAr =
+ noLongerVisible.toArray(new NotificationVisibility[noLongerVisible.size()]);
+ mUiOffloadThread.submit(() -> {
+ try {
+ mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr);
+ } catch (RemoteException e) {
+ // Ignore.
+ }
+
+ final int N = newlyVisible.size();
+ if (N > 0) {
+ String[] newlyVisibleKeyAr = new String[N];
+ for (int i = 0; i < N; i++) {
+ newlyVisibleKeyAr[i] = newlyVisibleAr[i].key;
+ }
+
+ // TODO: Call NotificationEntryManager to do this, once it exists.
+ // TODO: Consider not catching all runtime exceptions here.
+ try {
+ mNotificationListener.setNotificationsShown(newlyVisibleKeyAr);
+ } catch (RuntimeException e) {
+ Log.d(TAG, "failed setNotificationsShown: ", e);
+ }
+ }
+ });
+ }
+
+ private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
+ final int N = array.size();
+ for (int i = 0 ; i < N; i++) {
+ array.valueAt(i).recycle();
+ }
+ array.clear();
+ }
+
+ @VisibleForTesting
+ public Runnable getVisibilityReporter() {
+ return mVisibilityReporter;
+ }
+
+ /**
+ * A listener that is notified when some child locations might have changed.
+ */
+ public interface OnChildLocationsChangedListener {
+ void onChildLocationsChanged();
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationMediaManager.java b/com/android/systemui/statusbar/NotificationMediaManager.java
index e65bab29..852239a2 100644
--- a/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -1,3 +1,18 @@
+/*
+ * 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 com.android.systemui.statusbar;
import android.app.Notification;
@@ -25,9 +40,11 @@ public class NotificationMediaManager implements Dumpable {
private static final String TAG = "NotificationMediaManager";
public static final boolean DEBUG_MEDIA = false;
- private final NotificationPresenter mPresenter;
private final Context mContext;
private final MediaSessionManager mMediaSessionManager;
+
+ protected NotificationPresenter mPresenter;
+ protected NotificationEntryManager mEntryManager;
private MediaController mMediaController;
private String mMediaNotificationKey;
private MediaMetadata mMediaMetadata;
@@ -58,8 +75,7 @@ public class NotificationMediaManager implements Dumpable {
}
};
- public NotificationMediaManager(NotificationPresenter presenter, Context context) {
- mPresenter = presenter;
+ public NotificationMediaManager(Context context) {
mContext = context;
mMediaSessionManager
= (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
@@ -67,6 +83,12 @@ public class NotificationMediaManager implements Dumpable {
// in session state
}
+ public void setUpWithPresenter(NotificationPresenter presenter,
+ NotificationEntryManager entryManager) {
+ mPresenter = presenter;
+ mEntryManager = entryManager;
+ }
+
public void onNotificationRemoved(String key) {
if (key.equals(mMediaNotificationKey)) {
clearCurrentMediaNotification();
@@ -85,8 +107,8 @@ public class NotificationMediaManager implements Dumpable {
public void findAndUpdateMediaNotifications() {
boolean metaDataChanged = false;
- synchronized (mPresenter.getNotificationData()) {
- ArrayList<NotificationData.Entry> activeNotifications = mPresenter
+ synchronized (mEntryManager.getNotificationData()) {
+ ArrayList<NotificationData.Entry> activeNotifications = mEntryManager
.getNotificationData().getActiveNotifications();
final int N = activeNotifications.size();
@@ -173,7 +195,7 @@ public class NotificationMediaManager implements Dumpable {
}
if (metaDataChanged) {
- mPresenter.updateNotifications();
+ mEntryManager.updateNotifications();
}
mPresenter.updateMediaMetaData(metaDataChanged, true);
}
diff --git a/com/android/systemui/statusbar/NotificationPresenter.java b/com/android/systemui/statusbar/NotificationPresenter.java
index 1aca60c9..12641a0e 100644
--- a/com/android/systemui/statusbar/NotificationPresenter.java
+++ b/com/android/systemui/statusbar/NotificationPresenter.java
@@ -16,7 +16,10 @@
package com.android.systemui.statusbar;
import android.content.Intent;
-import android.service.notification.NotificationListenerService;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
/**
* An abstraction of something that presents notifications, e.g. StatusBar. Contains methods
@@ -25,8 +28,11 @@ import android.service.notification.NotificationListenerService;
* for affecting the state of the system (e.g. starting an intent, given that the presenter may
* want to perform some action before doing so).
*/
-public interface NotificationPresenter {
-
+public interface NotificationPresenter extends NotificationData.Environment,
+ NotificationRemoteInputManager.Callback,
+ ExpandableNotificationRow.OnExpandClickListener,
+ ActivatableNotificationView.OnActivatedListener,
+ NotificationEntryManager.Callback {
/**
* Returns true if the presenter is not visible. For example, it may not be necessary to do
* animations if this returns true.
@@ -39,41 +45,78 @@ public interface NotificationPresenter {
boolean isPresenterLocked();
/**
- * Returns the current user id. This can change if the user is switched.
- */
- int getCurrentUserId();
-
- /**
* Runs the given intent. The presenter may want to run some animations or close itself when
* this happens.
*/
void startNotificationGutsIntent(Intent intent, int appUid);
/**
- * Returns NotificationData.
+ * Returns the Handler for NotificationPresenter.
*/
- NotificationData getNotificationData();
+ Handler getHandler();
- // TODO: Create NotificationEntryManager and move this method to there.
/**
- * Signals that some notifications have changed, and NotificationPresenter should update itself.
+ * Refresh or remove lockscreen artwork from media metadata or the lockscreen wallpaper.
*/
- void updateNotifications();
+ void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation);
/**
- * Refresh or remove lockscreen artwork from media metadata or the lockscreen wallpaper.
+ * Called when the locked status of the device is changed for a work profile.
*/
- void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation);
+ void onWorkChallengeChanged();
+
+ /**
+ * Called when the current user changes.
+ * @param newUserId new user id
+ */
+ void onUserSwitched(int newUserId);
+
+ /**
+ * Gets the NotificationLockscreenUserManager for this Presenter.
+ */
+ NotificationLockscreenUserManager getNotificationLockscreenUserManager();
+
+ /**
+ * Wakes the device up if dozing.
+ *
+ * @param time the time when the request to wake up was issued
+ * @param where which view caused this wake up request
+ */
+ void wakeUpIfDozing(long time, View where);
+
+ /**
+ * True if the device currently requires a PIN, pattern, or password to unlock.
+ *
+ * @param userId user id to query about
+ * @return true iff the device is locked
+ */
+ boolean isDeviceLocked(int userId);
+
+ /**
+ * @return true iff the device is in vr mode
+ */
+ boolean isDeviceInVrMode();
+
+ /**
+ * Updates the visual representation of the notifications.
+ */
+ void updateNotificationViews();
+
+ /**
+ * @return true iff the device is dozing
+ */
+ boolean isDozing();
- // TODO: Create NotificationUpdateHandler and move this method to there.
/**
- * Removes a notification.
+ * Returns the maximum number of notifications to show while locked.
+ *
+ * @param recompute whether something has changed that means we should recompute this value
+ * @return the maximum number of notifications to show while locked
*/
- void removeNotification(String key, NotificationListenerService.RankingMap ranking);
+ int getMaxNotificationsWhileLocked(boolean recompute);
- // TODO: Create NotificationEntryManager and move this method to there.
/**
- * Gets the latest ranking map.
+ * Called when the row states are updated by NotificationViewHierarchyManager.
*/
- NotificationListenerService.RankingMap getLatestRankingMap();
+ void onUpdateRowStates();
}
diff --git a/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/com/android/systemui/statusbar/NotificationRemoteInputManager.java
new file mode 100644
index 00000000..f25379ab
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -0,0 +1,446 @@
+/*
+ * 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 com.android.systemui.statusbar;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
+
+import android.app.ActivityManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserManager;
+import android.service.notification.StatusBarNotification;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.statusbar.policy.RemoteInputView;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Set;
+
+/**
+ * Class for handling remote input state over a set of notifications. This class handles things
+ * like keeping notifications temporarily that were cancelled as a response to a remote input
+ * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
+ * and handling clicks on remote views.
+ */
+public class NotificationRemoteInputManager implements Dumpable {
+ public static final boolean ENABLE_REMOTE_INPUT =
+ SystemProperties.getBoolean("debug.enable_remote_input", true);
+ public static final boolean FORCE_REMOTE_INPUT_HISTORY =
+ SystemProperties.getBoolean("debug.force_remoteinput_history", true);
+ private static final boolean DEBUG = false;
+ private static final String TAG = "NotificationRemoteInputManager";
+
+ /**
+ * How long to wait before auto-dismissing a notification that was kept for remote input, and
+ * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
+ * these given that they technically don't exist anymore. We wait a bit in case the app issues
+ * an update.
+ */
+ private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
+
+ protected final ArraySet<NotificationData.Entry> mRemoteInputEntriesToRemoveOnCollapse =
+ new ArraySet<>();
+
+ // Dependencies:
+ protected final NotificationLockscreenUserManager mLockscreenUserManager =
+ Dependency.get(NotificationLockscreenUserManager.class);
+
+ /**
+ * Notifications with keys in this set are not actually around anymore. We kept them around
+ * when they were canceled in response to a remote input interaction. This allows us to show
+ * what you replied and allows you to continue typing into it.
+ */
+ protected final ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>();
+ protected final Context mContext;
+ private final UserManager mUserManager;
+
+ protected RemoteInputController mRemoteInputController;
+ protected NotificationPresenter mPresenter;
+ protected NotificationEntryManager mEntryManager;
+ protected IStatusBarService mBarService;
+ protected Callback mCallback;
+
+ private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
+
+ @Override
+ public boolean onClickHandler(
+ final View view, final PendingIntent pendingIntent, final Intent fillInIntent) {
+ mPresenter.wakeUpIfDozing(SystemClock.uptimeMillis(), view);
+
+ if (handleRemoteInput(view, pendingIntent)) {
+ return true;
+ }
+
+ if (DEBUG) {
+ Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
+ }
+ logActionClick(view);
+ // The intent we are sending is for the application, which
+ // won't have permission to immediately start an activity after
+ // the user switches to home. We know it is safe to do at this
+ // point, so make sure new activity switches are now allowed.
+ try {
+ ActivityManager.getService().resumeAppSwitches();
+ } catch (RemoteException e) {
+ }
+ return mCallback.handleRemoteViewClick(view, pendingIntent, fillInIntent,
+ () -> superOnClickHandler(view, pendingIntent, fillInIntent));
+ }
+
+ private void logActionClick(View view) {
+ ViewParent parent = view.getParent();
+ String key = getNotificationKeyForParent(parent);
+ if (key == null) {
+ Log.w(TAG, "Couldn't determine notification for click.");
+ return;
+ }
+ int index = -1;
+ // If this is a default template, determine the index of the button.
+ if (view.getId() == com.android.internal.R.id.action0 &&
+ parent != null && parent instanceof ViewGroup) {
+ ViewGroup actionGroup = (ViewGroup) parent;
+ index = actionGroup.indexOfChild(view);
+ }
+ try {
+ mBarService.onNotificationActionClick(key, index);
+ } catch (RemoteException e) {
+ // Ignore
+ }
+ }
+
+ private String getNotificationKeyForParent(ViewParent parent) {
+ while (parent != null) {
+ if (parent instanceof ExpandableNotificationRow) {
+ return ((ExpandableNotificationRow) parent)
+ .getStatusBarNotification().getKey();
+ }
+ parent = parent.getParent();
+ }
+ return null;
+ }
+
+ private boolean superOnClickHandler(View view, PendingIntent pendingIntent,
+ Intent fillInIntent) {
+ return super.onClickHandler(view, pendingIntent, fillInIntent,
+ WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
+ }
+
+ private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
+ if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
+ return true;
+ }
+
+ Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
+ RemoteInput[] inputs = null;
+ if (tag instanceof RemoteInput[]) {
+ inputs = (RemoteInput[]) tag;
+ }
+
+ if (inputs == null) {
+ return false;
+ }
+
+ RemoteInput input = null;
+
+ for (RemoteInput i : inputs) {
+ if (i.getAllowFreeFormInput()) {
+ input = i;
+ }
+ }
+
+ if (input == null) {
+ return false;
+ }
+
+ ViewParent p = view.getParent();
+ RemoteInputView riv = null;
+ while (p != null) {
+ if (p instanceof View) {
+ View pv = (View) p;
+ if (pv.isRootNamespace()) {
+ riv = findRemoteInputView(pv);
+ break;
+ }
+ }
+ p = p.getParent();
+ }
+ ExpandableNotificationRow row = null;
+ while (p != null) {
+ if (p instanceof ExpandableNotificationRow) {
+ row = (ExpandableNotificationRow) p;
+ break;
+ }
+ p = p.getParent();
+ }
+
+ if (row == null) {
+ return false;
+ }
+
+ row.setUserExpanded(true);
+
+ if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
+ final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
+ if (mLockscreenUserManager.isLockscreenPublicMode(userId)) {
+ mCallback.onLockedRemoteInput(row, view);
+ return true;
+ }
+ if (mUserManager.getUserInfo(userId).isManagedProfile()
+ && mPresenter.isDeviceLocked(userId)) {
+ mCallback.onLockedWorkRemoteInput(userId, row, view);
+ return true;
+ }
+ }
+
+ if (riv == null) {
+ riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
+ if (riv == null) {
+ return false;
+ }
+ if (!row.getPrivateLayout().getExpandedChild().isShown()) {
+ mCallback.onMakeExpandedVisibleForRemoteInput(row, view);
+ return true;
+ }
+ }
+
+ int width = view.getWidth();
+ if (view instanceof TextView) {
+ // Center the reveal on the text which might be off-center from the TextView
+ TextView tv = (TextView) view;
+ if (tv.getLayout() != null) {
+ int innerWidth = (int) tv.getLayout().getLineWidth(0);
+ innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
+ width = Math.min(width, innerWidth);
+ }
+ }
+ int cx = view.getLeft() + width / 2;
+ int cy = view.getTop() + view.getHeight() / 2;
+ int w = riv.getWidth();
+ int h = riv.getHeight();
+ int r = Math.max(
+ Math.max(cx + cy, cx + (h - cy)),
+ Math.max((w - cx) + cy, (w - cx) + (h - cy)));
+
+ riv.setRevealParameters(cx, cy, r);
+ riv.setPendingIntent(pendingIntent);
+ riv.setRemoteInput(inputs, input);
+ riv.focusAnimated();
+
+ return true;
+ }
+
+ private RemoteInputView findRemoteInputView(View v) {
+ if (v == null) {
+ return null;
+ }
+ return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
+ }
+ };
+
+ public NotificationRemoteInputManager(Context context) {
+ mContext = context;
+ mBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ }
+
+ public void setUpWithPresenter(NotificationPresenter presenter,
+ NotificationEntryManager entryManager,
+ Callback callback,
+ RemoteInputController.Delegate delegate) {
+ mPresenter = presenter;
+ mEntryManager = entryManager;
+ mCallback = callback;
+ mRemoteInputController = new RemoteInputController(delegate);
+ mRemoteInputController.addCallback(new RemoteInputController.Callback() {
+ @Override
+ public void onRemoteInputSent(NotificationData.Entry entry) {
+ if (FORCE_REMOTE_INPUT_HISTORY && mKeysKeptForRemoteInput.contains(entry.key)) {
+ mEntryManager.removeNotification(entry.key, null);
+ } else if (mRemoteInputEntriesToRemoveOnCollapse.contains(entry)) {
+ // We're currently holding onto this notification, but from the apps point of
+ // view it is already canceled, so we'll need to cancel it on the apps behalf
+ // after sending - unless the app posts an update in the mean time, so wait a
+ // bit.
+ mPresenter.getHandler().postDelayed(() -> {
+ if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) {
+ mEntryManager.removeNotification(entry.key, null);
+ }
+ }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
+ }
+ }
+ });
+
+ }
+
+ public RemoteInputController getController() {
+ return mRemoteInputController;
+ }
+
+ public void onUpdateNotification(NotificationData.Entry entry) {
+ mRemoteInputEntriesToRemoveOnCollapse.remove(entry);
+ }
+
+ /**
+ * Returns true if NotificationRemoteInputManager wants to keep this notification around.
+ *
+ * @param entry notification being removed
+ */
+ public boolean onRemoveNotification(NotificationData.Entry entry) {
+ if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
+ && (entry.row != null && !entry.row.isDismissed())) {
+ mRemoteInputEntriesToRemoveOnCollapse.add(entry);
+ return true;
+ }
+ return false;
+ }
+
+ public void onPerformRemoveNotification(StatusBarNotification n,
+ NotificationData.Entry entry) {
+ if (mRemoteInputController.isRemoteInputActive(entry)) {
+ mRemoteInputController.removeRemoteInput(entry, null);
+ }
+ if (FORCE_REMOTE_INPUT_HISTORY
+ && mKeysKeptForRemoteInput.contains(n.getKey())) {
+ mKeysKeptForRemoteInput.remove(n.getKey());
+ }
+ }
+
+ public void removeRemoteInputEntriesKeptUntilCollapsed() {
+ for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) {
+ NotificationData.Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i);
+ mRemoteInputController.removeRemoteInput(entry, null);
+ mEntryManager.removeNotification(entry.key, mEntryManager.getLatestRankingMap());
+ }
+ mRemoteInputEntriesToRemoveOnCollapse.clear();
+ }
+
+ public void checkRemoteInputOutside(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
+ && event.getX() == 0 && event.getY() == 0 // a touch outside both bars
+ && mRemoteInputController.isRemoteInputActive()) {
+ mRemoteInputController.closeRemoteInputs();
+ }
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("NotificationRemoteInputManager state:");
+ pw.print(" mRemoteInputEntriesToRemoveOnCollapse: ");
+ pw.println(mRemoteInputEntriesToRemoveOnCollapse);
+ pw.print(" mKeysKeptForRemoteInput: ");
+ pw.println(mKeysKeptForRemoteInput);
+ }
+
+ public void bindRow(ExpandableNotificationRow row) {
+ row.setRemoteInputController(mRemoteInputController);
+ row.setRemoteViewClickHandler(mOnClickHandler);
+ }
+
+ public Set<String> getKeysKeptForRemoteInput() {
+ return mKeysKeptForRemoteInput;
+ }
+
+ @VisibleForTesting
+ public Set<NotificationData.Entry> getRemoteInputEntriesToRemoveOnCollapse() {
+ return mRemoteInputEntriesToRemoveOnCollapse;
+ }
+
+ /**
+ * Callback for various remote input related events, or for providing information that
+ * NotificationRemoteInputManager needs to know to decide what to do.
+ */
+ public interface Callback {
+
+ /**
+ * Called when remote input was activated but the device is locked.
+ *
+ * @param row
+ * @param clicked
+ */
+ void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
+
+ /**
+ * Called when remote input was activated but the device is locked and in a managed profile.
+ *
+ * @param userId
+ * @param row
+ * @param clicked
+ */
+ void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
+
+ /**
+ * Called when a row should be made expanded for the purposes of remote input.
+ *
+ * @param row
+ * @param clickedView
+ */
+ void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
+
+ /**
+ * Return whether or not remote input should be handled for this view.
+ *
+ * @param view
+ * @param pendingIntent
+ * @return true iff the remote input should be handled
+ */
+ boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
+
+ /**
+ * Performs any special handling for a remote view click. The default behaviour can be
+ * called through the defaultHandler parameter.
+ *
+ * @param view
+ * @param pendingIntent
+ * @param fillInIntent
+ * @param defaultHandler
+ * @return true iff the click was handled
+ */
+ boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, Intent fillInIntent,
+ ClickHandler defaultHandler);
+ }
+
+ /**
+ * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
+ * so it may do its own handling before invoking the default behaviour.
+ */
+ public interface ClickHandler {
+ /**
+ * Tries to handle a click on a remote view.
+ *
+ * @return true iff the click was handled
+ */
+ boolean handleClick();
+ }
+}
diff --git a/com/android/systemui/statusbar/NotificationShelf.java b/com/android/systemui/statusbar/NotificationShelf.java
index b7a00ebc..8325df7f 100644
--- a/com/android/systemui/statusbar/NotificationShelf.java
+++ b/com/android/systemui/statusbar/NotificationShelf.java
@@ -25,6 +25,7 @@ import android.content.res.Resources;
import android.graphics.Rect;
import android.os.SystemProperties;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
@@ -56,6 +57,7 @@ public class NotificationShelf extends ActivatableNotificationView implements
private static final boolean ICON_ANMATIONS_WHILE_SCROLLING
= SystemProperties.getBoolean("debug.icon_scroll_animations", true);
private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
+ private static final String TAG = "NotificationShelf";
private ViewInvertHelper mViewInvertHelper;
private boolean mDark;
private NotificationIconContainer mShelfIcons;
@@ -82,9 +84,6 @@ public class NotificationShelf extends ActivatableNotificationView implements
private boolean mNoAnimationsInThisFrame;
private boolean mAnimationsEnabled = true;
private boolean mShowNotificationShelf;
- private boolean mVibrationOnAnimation;
- private boolean mUserTouchingScreen;
- private boolean mTouchActive;
private float mFirstElementRoundness;
public NotificationShelf(Context context, AttributeSet attrs) {
@@ -101,10 +100,7 @@ public class NotificationShelf extends ActivatableNotificationView implements
setClipToActualHeight(false);
setClipChildren(false);
setClipToPadding(false);
- mShelfIcons.setShowAllIcons(false);
- mVibrationOnAnimation = mContext.getResources().getBoolean(
- R.bool.config_vibrateOnIconAnimation);
- updateVibrationOnAnimation();
+ mShelfIcons.setIsStaticLayout(false);
mViewInvertHelper = new ViewInvertHelper(mShelfIcons,
NotificationPanelView.DOZE_ANIMATION_DURATION);
mShelfState = new ShelfState();
@@ -112,15 +108,6 @@ public class NotificationShelf extends ActivatableNotificationView implements
initDimens();
}
- private void updateVibrationOnAnimation() {
- mShelfIcons.setVibrateOnAnimation(mVibrationOnAnimation && mTouchActive);
- }
-
- public void setTouchActive(boolean touchActive) {
- mTouchActive = touchActive;
- updateVibrationOnAnimation();
- }
-
public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout) {
mAmbientState = ambientState;
mHostLayout = hostLayout;
@@ -309,10 +296,15 @@ public class NotificationShelf extends ActivatableNotificationView implements
if (notGoneIndex == 0) {
StatusBarIconView icon = row.getEntry().expandedIcon;
NotificationIconContainer.IconState iconState = getIconState(icon);
- if (iconState.clampedAppearAmount == 1.0f) {
+ if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
// only if the first icon is fully in the shelf we want to clip to it!
backgroundTop = (int) (row.getTranslationY() - getTranslationY());
firstElementRoundness = row.getCurrentTopRoundness();
+ } else if (iconState == null) {
+ Log.wtf(TAG, "iconState is null. ExpandedIcon: " + row.getEntry().expandedIcon
+ + (row.getEntry().expandedIcon != null
+ ? "\n icon parent: " + row.getEntry().expandedIcon.getParent() : "")
+ + " \n number of notifications: " + mHostLayout.getChildCount() );
}
}
notGoneIndex++;
@@ -696,7 +688,8 @@ public class NotificationShelf extends ActivatableNotificationView implements
if (isLayoutRtl()) {
start = getWidth() - start - mCollapsedIcons.getWidth();
}
- int width = (int) NotificationUtils.interpolate(start + mCollapsedIcons.getWidth(),
+ int width = (int) NotificationUtils.interpolate(
+ start + mCollapsedIcons.getFinalTranslationX(),
mShelfIcons.getWidth(),
openedAmount);
mShelfIcons.setActualLayoutWidth(width);
@@ -706,6 +699,9 @@ public class NotificationShelf extends ActivatableNotificationView implements
// we have to ensure that adding the low priority notification won't lead to an
// overflow
collapsedPadding -= (1.0f + OVERFLOW_EARLY_AMOUNT) * mCollapsedIcons.getIconSize();
+ } else {
+ // Partial overflow padding will fill enough space to add extra dots
+ collapsedPadding -= mCollapsedIcons.getPartialOverflowExtraPadding();
}
float padding = NotificationUtils.interpolate(collapsedPadding,
mShelfIcons.getPaddingEnd(),
@@ -715,7 +711,6 @@ public class NotificationShelf extends ActivatableNotificationView implements
mShelfIcons.getPaddingStart(), openedAmount);
mShelfIcons.setActualPaddingStart(paddingStart);
mShelfIcons.setOpenedAmount(openedAmount);
- mShelfIcons.setVisualOverflowAdaption(mCollapsedIcons.getVisualOverflowAdaption());
}
public void setMaxLayoutHeight(int maxLayoutHeight) {
diff --git a/com/android/systemui/statusbar/NotificationSnooze.java b/com/android/systemui/statusbar/NotificationSnooze.java
index 492ab44d..aea01277 100644
--- a/com/android/systemui/statusbar/NotificationSnooze.java
+++ b/com/android/systemui/statusbar/NotificationSnooze.java
@@ -16,7 +16,6 @@ package com.android.systemui.statusbar;
*/
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -234,7 +233,7 @@ public class NotificationSnooze extends LinearLayout
final int defaultSnooze = mParser.getInt(KEY_DEFAULT_SNOOZE,
resources.getInteger(R.integer.config_notification_snooze_time_default));
- final int[] snoozeTimes = parseIntArray(KEY_OPTIONS,
+ final int[] snoozeTimes = mParser.getIntArray(KEY_OPTIONS,
resources.getIntArray(R.array.config_notification_snooze_times));
for (int i = 0; i < snoozeTimes.length && i < sAccessibilityActions.length; i++) {
@@ -248,21 +247,6 @@ public class NotificationSnooze extends LinearLayout
return options;
}
- @VisibleForTesting
- int[] parseIntArray(final String key, final int[] defaultArray) {
- final String value = mParser.getString(key, null);
- if (value != null) {
- try {
- return Arrays.stream(value.split(":")).map(String::trim).mapToInt(
- Integer::parseInt).toArray();
- } catch (NumberFormatException e) {
- return defaultArray;
- }
- } else {
- return defaultArray;
- }
- }
-
private SnoozeOption createOption(int minutes, int accessibilityActionId) {
Resources res = getResources();
boolean showInHours = minutes >= 60;
diff --git a/com/android/systemui/statusbar/NotificationUpdateHandler.java b/com/android/systemui/statusbar/NotificationUpdateHandler.java
new file mode 100644
index 00000000..0044194e
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationUpdateHandler.java
@@ -0,0 +1,58 @@
+/*
+ * 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 com.android.systemui.statusbar;
+
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+
+/**
+ * Interface for accepting notification updates from {@link NotificationListener}.
+ */
+public interface NotificationUpdateHandler {
+ /**
+ * Add a new notification and update the current notification ranking map.
+ *
+ * @param notification Notification to add
+ * @param ranking RankingMap to update with
+ */
+ void addNotification(StatusBarNotification notification,
+ NotificationListenerService.RankingMap ranking);
+
+ /**
+ * Remove a notification and update the current notification ranking map.
+ *
+ * @param key Key identifying the notification to remove
+ * @param ranking RankingMap to update with
+ */
+ void removeNotification(String key, NotificationListenerService.RankingMap ranking);
+
+ /**
+ * Update a given notification and the current notification ranking map.
+ *
+ * @param notification Updated notification
+ * @param ranking RankingMap to update with
+ */
+ void updateNotification(StatusBarNotification notification,
+ NotificationListenerService.RankingMap ranking);
+
+ /**
+ * Update with a new notification ranking map.
+ *
+ * @param ranking RankingMap to update with
+ */
+ void updateNotificationRanking(NotificationListenerService.RankingMap ranking);
+}
diff --git a/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
new file mode 100644
index 00000000..266c09bc
--- /dev/null
+++ b/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -0,0 +1,345 @@
+/*
+ * 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 com.android.systemui.statusbar;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Stack;
+
+/**
+ * NotificationViewHierarchyManager manages updating the view hierarchy of notification views based
+ * on their group structure. For example, if a notification becomes bundled with another,
+ * NotificationViewHierarchyManager will update the view hierarchy to reflect that. It also will
+ * tell NotificationListContainer which notifications to display, and inform it of changes to those
+ * notifications that might affect their display.
+ */
+public class NotificationViewHierarchyManager {
+ private static final String TAG = "NotificationViewHierarchyManager";
+
+ private final HashMap<ExpandableNotificationRow, List<ExpandableNotificationRow>>
+ mTmpChildOrderMap = new HashMap<>();
+
+ // Dependencies:
+ protected final NotificationLockscreenUserManager mLockscreenUserManager =
+ Dependency.get(NotificationLockscreenUserManager.class);
+ protected final NotificationGroupManager mGroupManager =
+ Dependency.get(NotificationGroupManager.class);
+ protected final VisualStabilityManager mVisualStabilityManager =
+ Dependency.get(VisualStabilityManager.class);
+
+ /**
+ * {@code true} if notifications not part of a group should by default be rendered in their
+ * expanded state. If {@code false}, then only the first notification will be expanded if
+ * possible.
+ */
+ private final boolean mAlwaysExpandNonGroupedNotification;
+
+ private NotificationPresenter mPresenter;
+ private NotificationEntryManager mEntryManager;
+ private NotificationListContainer mListContainer;
+
+ public NotificationViewHierarchyManager(Context context) {
+ Resources res = context.getResources();
+ mAlwaysExpandNonGroupedNotification =
+ res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
+ }
+
+ public void setUpWithPresenter(NotificationPresenter presenter,
+ NotificationEntryManager entryManager, NotificationListContainer listContainer) {
+ mPresenter = presenter;
+ mEntryManager = entryManager;
+ mListContainer = listContainer;
+ }
+
+ /**
+ * Updates the visual representation of the notifications.
+ */
+ public void updateNotificationViews() {
+ ArrayList<NotificationData.Entry> activeNotifications = mEntryManager.getNotificationData()
+ .getActiveNotifications();
+ ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
+ final int N = activeNotifications.size();
+ for (int i = 0; i < N; i++) {
+ NotificationData.Entry ent = activeNotifications.get(i);
+ if (ent.row.isDismissed() || ent.row.isRemoved()) {
+ // we don't want to update removed notifications because they could
+ // temporarily become children if they were isolated before.
+ continue;
+ }
+ int userId = ent.notification.getUserId();
+
+ // Display public version of the notification if we need to redact.
+ // TODO: This area uses a lot of calls into NotificationLockscreenUserManager.
+ // We can probably move some of this code there.
+ boolean devicePublic = mLockscreenUserManager.isLockscreenPublicMode(
+ mLockscreenUserManager.getCurrentUserId());
+ boolean userPublic = devicePublic
+ || mLockscreenUserManager.isLockscreenPublicMode(userId);
+ boolean needsRedaction = mLockscreenUserManager.needsRedaction(ent);
+ boolean sensitive = userPublic && needsRedaction;
+ boolean deviceSensitive = devicePublic
+ && !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(
+ mLockscreenUserManager.getCurrentUserId());
+ ent.row.setSensitive(sensitive, deviceSensitive);
+ ent.row.setNeedsRedaction(needsRedaction);
+ if (mGroupManager.isChildInGroupWithSummary(ent.row.getStatusBarNotification())) {
+ ExpandableNotificationRow summary = mGroupManager.getGroupSummary(
+ ent.row.getStatusBarNotification());
+ List<ExpandableNotificationRow> orderedChildren =
+ mTmpChildOrderMap.get(summary);
+ if (orderedChildren == null) {
+ orderedChildren = new ArrayList<>();
+ mTmpChildOrderMap.put(summary, orderedChildren);
+ }
+ orderedChildren.add(ent.row);
+ } else {
+ toShow.add(ent.row);
+ }
+
+ }
+
+ ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
+ for (int i=0; i< mListContainer.getContainerChildCount(); i++) {
+ View child = mListContainer.getContainerChildAt(i);
+ if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
+ toRemove.add((ExpandableNotificationRow) child);
+ }
+ }
+
+ for (ExpandableNotificationRow remove : toRemove) {
+ if (mGroupManager.isChildInGroupWithSummary(remove.getStatusBarNotification())) {
+ // we are only transferring this notification to its parent, don't generate an
+ // animation
+ mListContainer.setChildTransferInProgress(true);
+ }
+ if (remove.isSummaryWithChildren()) {
+ remove.removeAllChildren();
+ }
+ mListContainer.removeContainerView(remove);
+ mListContainer.setChildTransferInProgress(false);
+ }
+
+ removeNotificationChildren();
+
+ for (int i = 0; i < toShow.size(); i++) {
+ View v = toShow.get(i);
+ if (v.getParent() == null) {
+ mVisualStabilityManager.notifyViewAddition(v);
+ mListContainer.addContainerView(v);
+ }
+ }
+
+ addNotificationChildrenAndSort();
+
+ // So after all this work notifications still aren't sorted correctly.
+ // Let's do that now by advancing through toShow and mListContainer in
+ // lock-step, making sure mListContainer matches what we see in toShow.
+ int j = 0;
+ for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
+ View child = mListContainer.getContainerChildAt(i);
+ if (!(child instanceof ExpandableNotificationRow)) {
+ // We don't care about non-notification views.
+ continue;
+ }
+
+ ExpandableNotificationRow targetChild = toShow.get(j);
+ if (child != targetChild) {
+ // Oops, wrong notification at this position. Put the right one
+ // here and advance both lists.
+ if (mVisualStabilityManager.canReorderNotification(targetChild)) {
+ mListContainer.changeViewPosition(targetChild, i);
+ } else {
+ mVisualStabilityManager.addReorderingAllowedCallback(mEntryManager);
+ }
+ }
+ j++;
+
+ }
+
+ mVisualStabilityManager.onReorderingFinished();
+ // clear the map again for the next usage
+ mTmpChildOrderMap.clear();
+
+ updateRowStates();
+
+ mListContainer.onNotificationViewUpdateFinished();
+ }
+
+ private void addNotificationChildrenAndSort() {
+ // Let's now add all notification children which are missing
+ boolean orderChanged = false;
+ for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
+ View view = mListContainer.getContainerChildAt(i);
+ if (!(view instanceof ExpandableNotificationRow)) {
+ // We don't care about non-notification views.
+ continue;
+ }
+
+ ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
+ List<ExpandableNotificationRow> children = parent.getNotificationChildren();
+ List<ExpandableNotificationRow> orderedChildren = mTmpChildOrderMap.get(parent);
+
+ for (int childIndex = 0; orderedChildren != null && childIndex < orderedChildren.size();
+ childIndex++) {
+ ExpandableNotificationRow childView = orderedChildren.get(childIndex);
+ if (children == null || !children.contains(childView)) {
+ if (childView.getParent() != null) {
+ Log.wtf(TAG, "trying to add a notification child that already has " +
+ "a parent. class:" + childView.getParent().getClass() +
+ "\n child: " + childView);
+ // This shouldn't happen. We can recover by removing it though.
+ ((ViewGroup) childView.getParent()).removeView(childView);
+ }
+ mVisualStabilityManager.notifyViewAddition(childView);
+ parent.addChildNotification(childView, childIndex);
+ mListContainer.notifyGroupChildAdded(childView);
+ }
+ }
+
+ // Finally after removing and adding has been performed we can apply the order.
+ orderChanged |= parent.applyChildOrder(orderedChildren, mVisualStabilityManager,
+ mEntryManager);
+ }
+ if (orderChanged) {
+ mListContainer.generateChildOrderChangedEvent();
+ }
+ }
+
+ private void removeNotificationChildren() {
+ // First let's remove all children which don't belong in the parents
+ ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
+ for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
+ View view = mListContainer.getContainerChildAt(i);
+ if (!(view instanceof ExpandableNotificationRow)) {
+ // We don't care about non-notification views.
+ continue;
+ }
+
+ ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
+ List<ExpandableNotificationRow> children = parent.getNotificationChildren();
+ List<ExpandableNotificationRow> orderedChildren = mTmpChildOrderMap.get(parent);
+
+ if (children != null) {
+ toRemove.clear();
+ for (ExpandableNotificationRow childRow : children) {
+ if ((orderedChildren == null
+ || !orderedChildren.contains(childRow))
+ && !childRow.keepInParent()) {
+ toRemove.add(childRow);
+ }
+ }
+ for (ExpandableNotificationRow remove : toRemove) {
+ parent.removeChildNotification(remove);
+ if (mEntryManager.getNotificationData().get(
+ remove.getStatusBarNotification().getKey()) == null) {
+ // We only want to add an animation if the view is completely removed
+ // otherwise it's just a transfer
+ mListContainer.notifyGroupChildRemoved(remove,
+ parent.getChildrenContainer());
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates expanded, dimmed and locked states of notification rows.
+ */
+ public void updateRowStates() {
+ final int N = mListContainer.getContainerChildCount();
+
+ int visibleNotifications = 0;
+ boolean isLocked = mPresenter.isPresenterLocked();
+ int maxNotifications = -1;
+ if (isLocked) {
+ maxNotifications = mPresenter.getMaxNotificationsWhileLocked(true /* recompute */);
+ }
+ mListContainer.setMaxDisplayedNotifications(maxNotifications);
+ Stack<ExpandableNotificationRow> stack = new Stack<>();
+ for (int i = N - 1; i >= 0; i--) {
+ View child = mListContainer.getContainerChildAt(i);
+ if (!(child instanceof ExpandableNotificationRow)) {
+ continue;
+ }
+ stack.push((ExpandableNotificationRow) child);
+ }
+ while(!stack.isEmpty()) {
+ ExpandableNotificationRow row = stack.pop();
+ NotificationData.Entry entry = row.getEntry();
+ boolean isChildNotification =
+ mGroupManager.isChildInGroupWithSummary(entry.notification);
+
+ row.setOnKeyguard(isLocked);
+
+ if (!isLocked) {
+ // If mAlwaysExpandNonGroupedNotification is false, then only expand the
+ // very first notification and if it's not a child of grouped notifications.
+ row.setSystemExpanded(mAlwaysExpandNonGroupedNotification
+ || (visibleNotifications == 0 && !isChildNotification
+ && !row.isLowPriority()));
+ }
+
+ entry.row.setShowAmbient(mPresenter.isDozing());
+ int userId = entry.notification.getUserId();
+ boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
+ entry.notification) && !entry.row.isRemoved();
+ boolean showOnKeyguard = mLockscreenUserManager.shouldShowOnKeyguard(entry
+ .notification);
+ if (suppressedSummary
+ || (mLockscreenUserManager.isLockscreenPublicMode(userId)
+ && !mLockscreenUserManager.shouldShowLockscreenNotifications())
+ || (isLocked && !showOnKeyguard)) {
+ entry.row.setVisibility(View.GONE);
+ } else {
+ boolean wasGone = entry.row.getVisibility() == View.GONE;
+ if (wasGone) {
+ entry.row.setVisibility(View.VISIBLE);
+ }
+ if (!isChildNotification && !entry.row.isRemoved()) {
+ if (wasGone) {
+ // notify the scroller of a child addition
+ mListContainer.generateAddAnimation(entry.row,
+ !showOnKeyguard /* fromMoreCard */);
+ }
+ visibleNotifications++;
+ }
+ }
+ if (row.isSummaryWithChildren()) {
+ List<ExpandableNotificationRow> notificationChildren =
+ row.getNotificationChildren();
+ int size = notificationChildren.size();
+ for (int i = size - 1; i >= 0; i--) {
+ stack.push(notificationChildren.get(i));
+ }
+ }
+ }
+
+ mPresenter.onUpdateRowStates();
+ }
+}
diff --git a/com/android/systemui/statusbar/RemoteInputController.java b/com/android/systemui/statusbar/RemoteInputController.java
index ff6c775c..97e3d22c 100644
--- a/com/android/systemui/statusbar/RemoteInputController.java
+++ b/com/android/systemui/statusbar/RemoteInputController.java
@@ -21,17 +21,24 @@ import com.android.systemui.Dependency;
import com.android.systemui.statusbar.phone.StatusBarWindowManager;
import com.android.systemui.statusbar.policy.RemoteInputView;
+import android.app.Notification;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Pair;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.List;
/**
* Keeps track of the currently active {@link RemoteInputView}s.
*/
public class RemoteInputController {
+ private static final boolean ENABLE_REMOTE_INPUT =
+ SystemProperties.getBoolean("debug.enable_remote_input", true);
private final ArrayList<Pair<WeakReference<NotificationData.Entry>, Object>> mOpen
= new ArrayList<>();
@@ -45,6 +52,53 @@ public class RemoteInputController {
}
/**
+ * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this
+ * via first-class API.
+ *
+ * TODO: Remove once enough apps specify remote inputs on their own.
+ */
+ public static void processForRemoteInput(Notification n, Context context) {
+ if (!ENABLE_REMOTE_INPUT) {
+ return;
+ }
+
+ if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") &&
+ (n.actions == null || n.actions.length == 0)) {
+ Notification.Action viableAction = null;
+ Notification.WearableExtender we = new Notification.WearableExtender(n);
+
+ List<Notification.Action> actions = we.getActions();
+ final int numActions = actions.size();
+
+ for (int i = 0; i < numActions; i++) {
+ Notification.Action action = actions.get(i);
+ if (action == null) {
+ continue;
+ }
+ RemoteInput[] remoteInputs = action.getRemoteInputs();
+ if (remoteInputs == null) {
+ continue;
+ }
+ for (RemoteInput ri : remoteInputs) {
+ if (ri.getAllowFreeFormInput()) {
+ viableAction = action;
+ break;
+ }
+ }
+ if (viableAction != null) {
+ break;
+ }
+ }
+
+ if (viableAction != null) {
+ Notification.Builder rebuilder = Notification.Builder.recoverBuilder(context, n);
+ rebuilder.setActions(viableAction);
+ rebuilder.build(); // will rewrite n
+ }
+ }
+ }
+
+ /**
* Adds a currently active remote input.
*
* @param entry the entry for which a remote input is now active.
diff --git a/com/android/systemui/statusbar/car/CarStatusBar.java b/com/android/systemui/statusbar/car/CarStatusBar.java
index 5941af24..3ebeb4d4 100644
--- a/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -247,19 +247,6 @@ public class CarStatusBar extends StatusBar implements
return null;
}
- /**
- * Returns the
- * {@link com.android.systemui.statusbar.ExpandableNotificationRow.LongPressListener} that will
- * be triggered when a notification card is long-pressed.
- */
- @Override
- protected ExpandableNotificationRow.LongPressListener getNotificationLongClicker() {
- // For the automative use case, we do not want to the user to be able to interact with
- // a notification other than a regular click. As a result, just return null for the
- // long click listener.
- return null;
- }
-
@Override
public void showBatteryView() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
@@ -335,8 +322,8 @@ public class CarStatusBar extends StatusBar implements
}
@Override
- public void userSwitched(int newUserId) {
- super.userSwitched(newUserId);
+ public void onUserSwitched(int newUserId) {
+ super.onUserSwitched(newUserId);
if (mFullscreenUserSwitcher != null) {
mFullscreenUserSwitcher.onUserSwitched(newUserId);
}
@@ -388,18 +375,6 @@ public class CarStatusBar extends StatusBar implements
}
@Override
- protected boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn) {
- // Because space is usually constrained in the auto use-case, there should not be a
- // pinned notification when the shade has been expanded. Ensure this by not pinning any
- // notification if the shade is already opened.
- if (mPanelExpanded) {
- return false;
- }
-
- return super.shouldPeek(entry, sbn);
- }
-
- @Override
public void animateExpandNotificationsPanel() {
// Because space is usually constrained in the auto use-case, there should not be a
// pinned notification when the shade has been expanded. Ensure this by removing all heads-
diff --git a/com/android/systemui/statusbar/notification/NotificationUtils.java b/com/android/systemui/statusbar/notification/NotificationUtils.java
index af393c91..7e2336c4 100644
--- a/com/android/systemui/statusbar/notification/NotificationUtils.java
+++ b/com/android/systemui/statusbar/notification/NotificationUtils.java
@@ -61,11 +61,6 @@ public class NotificationUtils {
return sLocationOffset[1] - sLocationBase[1];
}
- public static boolean isHapticFeedbackDisabled(Context context) {
- return Settings.System.getIntForUser(context.getContentResolver(),
- Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) == 0;
- }
-
/**
* @param dimenId the dimen to look up
* @return the font scaled dimen as if it were in sp but doesn't shrink sizes below dp
diff --git a/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
index 61f3130b..61cb61ce 100644
--- a/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
+++ b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
@@ -55,6 +55,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
private KeyguardMonitor mKeyguardMonitor;
private NetworkController mNetworkController;
private LinearLayout mSystemIconArea;
+ private View mClockView;
private View mNotificationIconAreaInner;
private int mDisabled1;
private StatusBar mStatusBarComponent;
@@ -93,6 +94,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
mDarkIconManager = new DarkIconManager(view.findViewById(R.id.statusIcons));
Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager);
mSystemIconArea = mStatusBar.findViewById(R.id.system_icon_area);
+ mClockView = mStatusBar.findViewById(R.id.clock);
mSignalClusterView = mStatusBar.findViewById(R.id.signal_cluster);
Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mSignalClusterView);
// Default to showing until we know otherwise.
@@ -197,10 +199,12 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
public void hideSystemIconArea(boolean animate) {
animateHide(mSystemIconArea, animate);
+ animateHide(mClockView, animate);
}
public void showSystemIconArea(boolean animate) {
animateShow(mSystemIconArea, animate);
+ animateShow(mClockView, animate);
}
public void hideNotificationIconArea(boolean animate) {
diff --git a/com/android/systemui/statusbar/phone/DozeParameters.java b/com/android/systemui/statusbar/phone/DozeParameters.java
index 3f57c2f9..6d85fb37 100644
--- a/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -26,6 +26,7 @@ import android.util.SparseBooleanArray;
import com.android.internal.hardware.AmbientDisplayConfiguration;
import com.android.systemui.R;
+import com.android.systemui.doze.AlwaysOnDisplayPolicy;
import java.io.PrintWriter;
@@ -37,10 +38,12 @@ public class DozeParameters {
private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
private static IntInOutMatcher sPickupSubtypePerformsProxMatcher;
+ private final AlwaysOnDisplayPolicy mAlwaysOnPolicy;
public DozeParameters(Context context) {
mContext = context;
mAmbientDisplayConfiguration = new AmbientDisplayConfiguration(mContext);
+ mAlwaysOnPolicy = new AlwaysOnDisplayPolicy(context);
}
public void dump(PrintWriter pw) {
@@ -83,6 +86,11 @@ public class DozeParameters {
return getPulseInDuration() + getPulseVisibleDuration() + getPulseOutDuration();
}
+ public float getScreenBrightnessDoze() {
+ return mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_screenBrightnessDoze) / 255f;
+ }
+
public int getPulseInDuration() {
return getInt("doze.pulse.duration.in", R.integer.doze_pulse_duration_in);
}
@@ -115,10 +123,52 @@ public class DozeParameters {
return getInt("doze.pickup.vibration.threshold", R.integer.doze_pickup_vibration_threshold);
}
+ /**
+ * For how long a wallpaper can be visible in AoD before it fades aways.
+ * @return duration in millis.
+ */
+ public long getWallpaperAodDuration() {
+ return mAlwaysOnPolicy.wallpaperVisibilityDuration;
+ }
+
+ /**
+ * How long it takes for the wallpaper fade away (Animation duration.)
+ * @return duration in millis.
+ */
+ public long getWallpaperFadeOutDuration() {
+ return mAlwaysOnPolicy.wallpaperFadeOutDuration;
+ }
+
+ /**
+ * Checks if always on is available and enabled for the current user.
+ * @return {@code true} if enabled and available.
+ */
public boolean getAlwaysOn() {
return mAmbientDisplayConfiguration.alwaysOnEnabled(UserHandle.USER_CURRENT);
}
+ /**
+ * Some screens need to be completely black before changing the display power mode,
+ * unexpected behavior might happen if this parameter isn't respected.
+ *
+ * @return {@code true} if screen needs to be completely black before a power transition.
+ */
+ public boolean getDisplayNeedsBlanking() {
+ return mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_displayBlanksAfterDoze);
+ }
+
+ /**
+ * Whether we can implement our own screen off animation or if we need
+ * to rely on DisplayPowerManager to dim the display.
+ *
+ * @return {@code true} if SystemUI can control the screen off animation.
+ */
+ public boolean getCanControlScreenOffAnimation() {
+ return !mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_dozeAfterScreenOff);
+ }
+
private boolean getBoolean(String propName, int resId) {
return SystemProperties.getBoolean(propName, mContext.getResources().getBoolean(resId));
}
diff --git a/com/android/systemui/statusbar/phone/FingerprintUnlockController.java b/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
index 80d4061b..a2b10131 100644
--- a/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
+++ b/com/android/systemui/statusbar/phone/FingerprintUnlockController.java
@@ -26,7 +26,7 @@ import android.util.Log;
import com.android.keyguard.KeyguardConstants;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.keyguard.LatencyTracker;
+import com.android.internal.util.LatencyTracker;
import com.android.systemui.Dependency;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
@@ -266,7 +266,6 @@ public class FingerprintUnlockController extends KeyguardUpdateMonitorCallback {
}
private void showBouncer() {
- mScrimController.transitionTo(ScrimState.BOUNCER);
mStatusBarKeyguardViewManager.animateCollapsePanels(
FINGERPRINT_COLLAPSE_SPEEDUP_FACTOR);
mPendingShowBouncer = false;
diff --git a/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index 99debee2..b71ebfdc 100644
--- a/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.phone;
-import android.app.ActivityManager;
import android.content.Context;
import android.os.Handler;
import android.os.UserHandle;
@@ -102,7 +101,7 @@ public class KeyguardBouncer {
return;
}
- final int activeUserId = ActivityManager.getCurrentUser();
+ final int activeUserId = KeyguardUpdateMonitor.getCurrentUser();
final boolean isSystemUser =
UserManager.isSplitSystemUser() && activeUserId == UserHandle.USER_SYSTEM;
final boolean allowDismissKeyguard = !isSystemUser && activeUserId == keyguardUserId;
@@ -122,6 +121,8 @@ public class KeyguardBouncer {
// Split up the work over multiple frames.
DejankUtils.postAfterTraversal(mShowRunnable);
+
+ mCallback.onBouncerVisiblityChanged(true /* shown */);
}
private final Runnable mShowRunnable = new Runnable() {
@@ -182,6 +183,7 @@ public class KeyguardBouncer {
mDismissCallbackRegistry.notifyDismissCancelled();
}
mFalsingManager.onBouncerHidden();
+ mCallback.onBouncerVisiblityChanged(false /* shown */);
cancelShowRunnable();
if (mKeyguardView != null) {
mKeyguardView.cancelDismissAction();
diff --git a/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
index 316bd5bc..7f4deb03 100644
--- a/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
+++ b/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
@@ -61,14 +61,10 @@ public class ManagedProfileControllerImpl implements ManagedProfileController {
public void setWorkModeEnabled(boolean enableWorkMode) {
synchronized (mProfiles) {
for (UserInfo ui : mProfiles) {
- if (enableWorkMode) {
- if (!mUserManager.trySetQuietModeDisabled(ui.id, null)) {
- StatusBarManager statusBarManager = (StatusBarManager) mContext
- .getSystemService(android.app.Service.STATUS_BAR_SERVICE);
- statusBarManager.collapsePanels();
- }
- } else {
- mUserManager.setQuietModeEnabled(ui.id, true);
+ if (!mUserManager.trySetQuietModeEnabled(!enableWorkMode, UserHandle.of(ui.id))) {
+ StatusBarManager statusBarManager = (StatusBarManager) mContext
+ .getSystemService(android.app.Service.STATUS_BAR_SERVICE);
+ statusBarManager.collapsePanels();
}
}
}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
index 6d3bc1df..695168e3 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -67,8 +67,9 @@ import android.view.accessibility.AccessibilityManager.AccessibilityServicesStat
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.keyguard.LatencyTracker;
+import com.android.internal.util.LatencyTracker;
import com.android.systemui.Dependency;
+import com.android.systemui.OverviewProxyService;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.assist.AssistManager;
@@ -125,6 +126,8 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
private int mSystemUiVisibility;
private LightBarController mLightBarController;
+ private OverviewProxyService mOverviewProxyService;
+
public boolean mHomeBlockedThisTouch;
// ----- Fragment Lifecycle Callbacks -----
@@ -152,6 +155,7 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
mDisabledFlags1 = savedInstanceState.getInt(EXTRA_DISABLE_STATE, 0);
}
mAssistManager = Dependency.get(AssistManager.class);
+ mOverviewProxyService = Dependency.get(OverviewProxyService.class);
try {
WindowManagerGlobal.getWindowManagerService()
@@ -364,7 +368,8 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
private boolean shouldDisableNavbarGestures() {
return !mStatusBar.isDeviceProvisioned()
- || (mDisabledFlags1 & StatusBarManager.DISABLE_SEARCH) != 0;
+ || (mDisabledFlags1 & StatusBarManager.DISABLE_SEARCH) != 0
+ || mOverviewProxyService.getProxy() != null;
}
private void repositionNavigationBar() {
@@ -449,6 +454,7 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
MetricsLogger.action(getContext(), MetricsEvent.ACTION_ASSIST_LONG_PRESS);
mAssistManager.startAssist(new Bundle() /* args */);
mStatusBar.awakenDreams();
+
if (mNavigationBarView != null) {
mNavigationBarView.abortCurrentGesture();
}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarView.java b/com/android/systemui/statusbar/phone/NavigationBarView.java
index 4e7f205f..2796f0ff 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -98,6 +98,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
private GestureHelper mGestureHelper;
private DeadZone mDeadZone;
private final NavigationBarTransitions mBarTransitions;
+ private final OverviewProxyService mOverviewProxyService;
// workaround for LayoutTransitions leaving the nav buttons in a weird state (bug 5549288)
final static boolean WORKAROUND_INVALID_LAYOUT = true;
@@ -226,6 +227,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
mButtonDispatchers.put(R.id.ime_switcher, new ButtonDispatcher(R.id.ime_switcher));
mButtonDispatchers.put(R.id.accessibility_button,
new ButtonDispatcher(R.id.accessibility_button));
+ mOverviewProxyService = Dependency.get(OverviewProxyService.class);
}
public BarTransitions getBarTransitions() {
@@ -464,6 +466,10 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
disableBack = false;
disableRecent = false;
}
+ if (mOverviewProxyService.getProxy() != null) {
+ // When overview is connected to the launcher service, disable the recents button
+ disableRecent = true;
+ }
ViewGroup navButtons = (ViewGroup) getCurrentView().findViewById(R.id.nav_buttons);
if (navButtons != null) {
@@ -779,7 +785,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
onPluginDisconnected(null); // Create default gesture helper
Dependency.get(PluginManager.class).addPluginListener(this,
NavGesture.class, false /* Only one */);
- Dependency.get(OverviewProxyService.class).addCallback(mOverviewProxyListener);
+ mOverviewProxyService.addCallback(mOverviewProxyListener);
}
@Override
@@ -789,7 +795,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
if (mGestureHelper != null) {
mGestureHelper.destroy();
}
- Dependency.get(OverviewProxyService.class).removeCallback(mOverviewProxyListener);
+ mOverviewProxyService.removeCallback(mOverviewProxyListener);
}
@Override
diff --git a/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 836efffb..91cae0af 100644
--- a/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.phone;
-import static com.android.systemui.statusbar.notification.NotificationUtils.isHapticFeedbackDisabled;
-
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
@@ -102,8 +100,10 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
}.setDuration(200).setDelay(50);
public static final int MAX_VISIBLE_ICONS_WHEN_DARK = 5;
+ public static final int MAX_STATIC_ICONS = 4;
+ private static final int MAX_DOTS = 3;
- private boolean mShowAllIcons = true;
+ private boolean mIsStaticLayout = true;
private final HashMap<View, IconState> mIconStates = new HashMap<>();
private int mDotPadding;
private int mStaticDotRadius;
@@ -117,19 +117,18 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
private int mSpeedBumpIndex = -1;
private int mIconSize;
private float mOpenedAmount = 0.0f;
- private float mVisualOverflowAdaption;
private boolean mDisallowNextAnimation;
private boolean mAnimationsEnabled = true;
- private boolean mVibrateOnAnimation;
- private Vibrator mVibrator;
private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
private int mDarkOffsetX;
+ // Keep track of the last visible icon so collapsed container can report on its location
+ private IconState mLastVisibleIconState;
+
public NotificationIconContainer(Context context, AttributeSet attrs) {
super(context, attrs);
initDimens();
setWillNotDraw(!DEBUG);
- mVibrator = mContext.getSystemService(Vibrator.class);
}
private void initDimens() {
@@ -168,7 +167,7 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
mIconSize = child.getWidth();
}
}
- if (mShowAllIcons) {
+ if (mIsStaticLayout) {
resetViewStates();
calculateIconTranslations();
applyIconStates();
@@ -292,7 +291,8 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
float translationX = getActualPaddingStart();
int firstOverflowIndex = -1;
int childCount = getChildCount();
- int maxVisibleIcons = mDark ? MAX_VISIBLE_ICONS_WHEN_DARK : childCount;
+ int maxVisibleIcons = mDark ? MAX_VISIBLE_ICONS_WHEN_DARK :
+ mIsStaticLayout ? MAX_STATIC_ICONS : childCount;
float layoutEnd = getLayoutEnd();
float overflowStart = layoutEnd - mIconSize * (2 + OVERFLOW_EARLY_AMOUNT);
boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount();
@@ -325,23 +325,6 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
visualOverflowStart += (translationX - overflowStart) / mIconSize
* (mStaticDotRadius * 2 + mDotPadding);
}
- if (mShowAllIcons) {
- // We want to perfectly position the overflow in the static state, such that
- // it's perfectly centered instead of measuring it from the end.
- mVisualOverflowAdaption = 0;
- if (firstOverflowIndex != -1) {
- View firstOverflowView = getChildAt(i);
- IconState overflowState = mIconStates.get(firstOverflowView);
- float totalAmount = layoutEnd - overflowState.xTranslation;
- float newPosition = overflowState.xTranslation + totalAmount / 2
- - totalDotLength / 2
- - mIconSize * 0.5f + mStaticDotRadius;
- mVisualOverflowAdaption = newPosition - visualOverflowStart;
- visualOverflowStart = newPosition;
- }
- } else {
- visualOverflowStart += mVisualOverflowAdaption * (1f - mOpenedAmount);
- }
}
translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
}
@@ -353,20 +336,24 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
IconState iconState = mIconStates.get(view);
int dotWidth = mStaticDotRadius * 2 + mDotPadding;
iconState.xTranslation = translationX;
- if (numDots <= 3) {
+ if (numDots <= MAX_DOTS) {
if (numDots == 1 && iconState.iconAppearAmount < 0.8f) {
iconState.visibleState = StatusBarIconView.STATE_ICON;
numDots--;
} else {
iconState.visibleState = StatusBarIconView.STATE_DOT;
}
- translationX += (numDots == 3 ? 3 * dotWidth : dotWidth)
+ translationX += (numDots == MAX_DOTS ? MAX_DOTS * dotWidth : dotWidth)
* iconState.iconAppearAmount;
+ mLastVisibleIconState = iconState;
} else {
iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
}
numDots++;
}
+ } else if (childCount > 0) {
+ View lastChild = getChildAt(childCount - 1);
+ mLastVisibleIconState = mIconStates.get(lastChild);
}
boolean center = mDark;
if (center && translationX < getLayoutEnd()) {
@@ -420,13 +407,13 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
}
/**
- * Sets whether the layout should always show all icons.
+ * Sets whether the layout should always show the same number of icons.
* If this is true, the icon positions will be updated on layout.
* If this if false, the layout is managed from the outside and layouting won't trigger a
* repositioning of the icons.
*/
- public void setShowAllIcons(boolean showAllIcons) {
- mShowAllIcons = showAllIcons;
+ public void setIsStaticLayout(boolean isStaticLayout) {
+ mIsStaticLayout = isStaticLayout;
}
public void setActualLayoutWidth(int actualLayoutWidth) {
@@ -457,6 +444,14 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
return mActualLayoutWidth;
}
+ public int getFinalTranslationX() {
+ if (mLastVisibleIconState == null) {
+ return 0;
+ }
+
+ return (int) (mLastVisibleIconState.xTranslation + mIconSize * (1 + OVERFLOW_EARLY_AMOUNT));
+ }
+
public void setChangingViewPositions(boolean changingViewPositions) {
mChangingViewPositions = changingViewPositions;
}
@@ -484,21 +479,41 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
mOpenedAmount = expandAmount;
}
- public float getVisualOverflowAdaption() {
- return mVisualOverflowAdaption;
- }
-
- public void setVisualOverflowAdaption(float visualOverflowAdaption) {
- mVisualOverflowAdaption = visualOverflowAdaption;
- }
-
public boolean hasOverflow() {
+ if (mIsStaticLayout) {
+ return getChildCount() > MAX_STATIC_ICONS;
+ }
+
float width = (getChildCount() + OVERFLOW_EARLY_AMOUNT) * mIconSize;
return width - (getWidth() - getActualPaddingStart() - getActualPaddingEnd()) > 0;
}
- public void setVibrateOnAnimation(boolean vibrateOnAnimation) {
- mVibrateOnAnimation = vibrateOnAnimation;
+ /**
+ * If the overflow is in the range [1, max_dots - 1) (basically 1 or 2 dots), then
+ * extra padding will have to be accounted for
+ *
+ * This method has no meaning for non-static containers
+ */
+ public boolean hasPartialOverflow() {
+ if (mIsStaticLayout) {
+ int count = getChildCount();
+ return count > MAX_STATIC_ICONS && count <= MAX_STATIC_ICONS + MAX_DOTS;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get padding that can account for extra dots up to the max. The only valid values for
+ * this method are for 1 or 2 dots.
+ * @return only extraDotPadding or extraDotPadding * 2
+ */
+ public int getPartialOverflowExtraPadding() {
+ if (!hasPartialOverflow()) {
+ return 0;
+ }
+
+ return (MAX_STATIC_ICONS + MAX_DOTS - getChildCount()) * (mStaticDotRadius + mDotPadding);
}
public int getIconSize() {
@@ -608,39 +623,14 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
} else {
super.applyToView(view);
}
- boolean wasInShelf = icon.isInShelf();
boolean inShelf = iconAppearAmount == 1.0f;
icon.setIsInShelf(inShelf);
- if (shouldVibrateChange(wasInShelf != inShelf)) {
- AsyncTask.execute(
- () -> mVibrator.vibrate(VibrationEffect.get(
- VibrationEffect.EFFECT_TICK)));
- }
}
justAdded = false;
justReplaced = false;
needsCannedAnimation = false;
}
- private boolean shouldVibrateChange(boolean inShelfChanged) {
- if (!mVibrateOnAnimation) {
- return false;
- }
- if (justAdded) {
- return false;
- }
- if (!mAnimationsEnabled) {
- return false;
- }
- if (!inShelfChanged) {
- return false;
- }
- if (isHapticFeedbackDisabled(mContext)) {
- return false;
- }
- return true;
- }
-
public boolean hasCustomTransformHeight() {
return isLastExpandIcon && customTransformHeight != NO_VALUE;
}
diff --git a/com/android/systemui/statusbar/phone/NotificationPanelView.java b/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 17e35999..f0bd1f94 100644
--- a/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -455,7 +455,7 @@ public class NotificationPanelView extends PanelView implements
mTopPaddingAdjustment = 0;
} else {
mClockPositionAlgorithm.setup(
- mStatusBar.getMaxKeyguardNotifications(),
+ mStatusBar.getMaxNotificationsWhileLocked(),
getMaxPanelHeight(),
getExpandedHeight(),
mNotificationStackScroller.getNotGoneChildCount(),
@@ -506,7 +506,8 @@ public class NotificationPanelView extends PanelView implements
if (suppressedSummary) {
continue;
}
- if (!mStatusBar.shouldShowOnKeyguard(row.getStatusBarNotification())) {
+ if (!mStatusBar.getNotificationLockscreenUserManager().shouldShowOnKeyguard(
+ row.getStatusBarNotification())) {
continue;
}
if (row.isRemoved()) {
diff --git a/com/android/systemui/statusbar/phone/PanelView.java b/com/android/systemui/statusbar/phone/PanelView.java
index afe5c917..2fc22caa 100644
--- a/com/android/systemui/statusbar/phone/PanelView.java
+++ b/com/android/systemui/statusbar/phone/PanelView.java
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.phone;
-import static com.android.systemui.statusbar.notification.NotificationUtils.isHapticFeedbackDisabled;
-
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
@@ -25,7 +23,9 @@ import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.database.ContentObserver;
import android.os.AsyncTask;
+import android.os.Handler;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.VibrationEffect;
@@ -42,7 +42,7 @@ import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.keyguard.LatencyTracker;
+import com.android.internal.util.LatencyTracker;
import com.android.systemui.DejankUtils;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
@@ -50,7 +50,6 @@ import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.statusbar.FlingAnimationUtils;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import java.io.FileDescriptor;
@@ -66,6 +65,7 @@ public abstract class PanelView extends FrameLayout {
private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
private boolean mPanelUpdateWhenAnimatorEnds;
private boolean mVibrateOnOpening;
+ private boolean mVibrationEnabled;
private final void logf(String fmt, Object... args) {
Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
@@ -108,6 +108,12 @@ public abstract class PanelView extends FrameLayout {
private FlingAnimationUtils mFlingAnimationUtilsDismissing;
private FalsingManager mFalsingManager;
private final Vibrator mVibrator;
+ final private ContentObserver mVibrationObserver = new ContentObserver(Handler.getMain()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ updateHapticFeedBackEnabled();
+ }
+ };
/**
* Whether an instant expand request is currently pending and we are just waiting for layout.
@@ -212,6 +218,15 @@ public abstract class PanelView extends FrameLayout {
mVibrator = mContext.getSystemService(Vibrator.class);
mVibrateOnOpening = mContext.getResources().getBoolean(
R.bool.config_vibrateOnIconAnimation);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_ENABLED), true,
+ mVibrationObserver);
+ mVibrationObserver.onChange(false /* selfChange */);
+ }
+
+ public void updateHapticFeedBackEnabled() {
+ mVibrationEnabled = Settings.System.getIntForUser(mContext.getContentResolver(),
+ Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) != 0;
}
protected void loadDimens() {
@@ -403,7 +418,7 @@ public abstract class PanelView extends FrameLayout {
runPeekAnimation(INITIAL_OPENING_PEEK_DURATION, getOpeningHeight(),
false /* collapseWhenFinished */);
notifyBarPanelExpansionChanged();
- if (mVibrateOnOpening && !isHapticFeedbackDisabled(mContext)) {
+ if (mVibrateOnOpening && mVibrationEnabled) {
AsyncTask.execute(() ->
mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_TICK, false)));
}
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 09fe579b..f41cb293 100644
--- a/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -615,6 +615,7 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks,
.addCategory(Intent.CATEGORY_BROWSABLE)
.addCategory("unique:" + System.currentTimeMillis())
.putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName)
+ .putExtra(Intent.EXTRA_VERSION_CODE, (int) (appInfo.versionCode & 0x7fffffff))
.putExtra(Intent.EXTRA_VERSION_CODE, appInfo.versionCode)
.putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, pendingIntent);
diff --git a/com/android/systemui/statusbar/phone/ScrimController.java b/com/android/systemui/statusbar/phone/ScrimController.java
index 3a367763..14329b56 100644
--- a/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/com/android/systemui/statusbar/phone/ScrimController.java
@@ -20,6 +20,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
+import android.app.AlarmManager;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Color;
@@ -51,6 +52,7 @@ import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.stack.ViewState;
+import com.android.systemui.util.AlarmTimeout;
import com.android.systemui.util.wakelock.DelayedWakeLock;
import com.android.systemui.util.wakelock.WakeLock;
@@ -73,6 +75,19 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
= new PathInterpolator(0f, 0, 0.7f, 1f);
public static final Interpolator KEYGUARD_FADE_OUT_INTERPOLATOR_LOCKED
= new PathInterpolator(0.3f, 0f, 0.8f, 1f);
+
+ /**
+ * When both scrims have 0 alpha.
+ */
+ public static final int VISIBILITY_FULLY_TRANSPARENT = 0;
+ /**
+ * When scrims aren't transparent (alpha 0) but also not opaque (alpha 1.)
+ */
+ public static final int VISIBILITY_SEMI_TRANSPARENT = 1;
+ /**
+ * When at least 1 scrim is fully opaque (alpha set to 1.)
+ */
+ public static final int VISIBILITY_FULLY_OPAQUE = 2;
/**
* Default alpha value for most scrims.
*/
@@ -111,6 +126,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
private final UnlockMethodCache mUnlockMethodCache;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private final DozeParameters mDozeParameters;
+ private final AlarmTimeout mTimeTicker;
private final SysuiColorExtractor mColorExtractor;
private GradientColors mLockColors;
@@ -138,23 +154,25 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
private float mCurrentBehindAlpha = NOT_INITIALIZED;
private int mCurrentInFrontTint;
private int mCurrentBehindTint;
+ private boolean mWallpaperVisibilityTimedOut;
private int mPinnedHeadsUpCount;
private float mTopHeadsUpDragAmount;
private View mDraggedHeadsUpView;
private boolean mKeyguardFadingOutInProgress;
private ValueAnimator mKeyguardFadeoutAnimation;
- private boolean mScrimsVisible;
- private final Consumer<Boolean> mScrimVisibleListener;
+ private int mScrimsVisibility;
+ private final Consumer<Integer> mScrimVisibleListener;
private boolean mBlankScreen;
private boolean mScreenBlankingCallbackCalled;
private Callback mCallback;
+ private boolean mWallpaperSupportsAmbientMode;
private final WakeLock mWakeLock;
private boolean mWakeLockHeld;
public ScrimController(LightBarController lightBarController, ScrimView scrimBehind,
- ScrimView scrimInFront, View headsUpScrim, Consumer<Boolean> scrimVisibleListener,
- DozeParameters dozeParameters) {
+ ScrimView scrimInFront, View headsUpScrim, Consumer<Integer> scrimVisibleListener,
+ DozeParameters dozeParameters, AlarmManager alarmManager) {
mScrimBehind = scrimBehind;
mScrimInFront = scrimInFront;
mHeadsUpScrim = headsUpScrim;
@@ -164,6 +182,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
mLightBarController = lightBarController;
mScrimBehindAlphaResValue = mContext.getResources().getFloat(R.dimen.scrim_behind_alpha);
+ mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout,
+ "hide_aod_wallpaper", new Handler());
mWakeLock = createWakeLock();
// Scrim alpha is initially set to the value on the resource but might be changed
// to make sure that text on top of it is legible.
@@ -195,6 +215,10 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
public void transitionTo(ScrimState state, Callback callback) {
if (state == mState) {
+ // Call the callback anyway, unless it's already enqueued
+ if (callback != null && mCallback != callback) {
+ callback.onFinished();
+ }
return;
} else if (DEBUG) {
Log.d(TAG, "State changed to: " + state);
@@ -204,12 +228,15 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
throw new IllegalArgumentException("Cannot change to UNINITIALIZED.");
}
+ final ScrimState oldState = mState;
+ mState = state;
+
if (mCallback != null) {
mCallback.onCancelled();
}
mCallback = callback;
- state.prepare(mState);
+ state.prepare(oldState);
mScreenBlankingCallbackCalled = false;
mAnimationDelay = 0;
mBlankScreen = state.getBlanksScreen();
@@ -228,16 +255,24 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
mKeyguardFadeoutAnimation.cancel();
}
- mState = state;
+ // The device might sleep if it's entering AOD, we need to make sure that
+ // the animation plays properly until the last frame.
+ // It's important to avoid holding the wakelock unless necessary because
+ // WakeLock#aqcuire will trigger an IPC and will cause jank.
+ if (mState == ScrimState.AOD) {
+ holdWakeLock();
+ }
- // Do not let the device sleep until we're done with all animations
- if (!mWakeLockHeld) {
- if (mWakeLock != null) {
- mWakeLockHeld = true;
- mWakeLock.acquire();
- } else {
- Log.w(TAG, "Cannot hold wake lock, it has not been set yet");
+ // AOD wallpapers should fade away after a while
+ if (mWallpaperSupportsAmbientMode && mDozeParameters.getAlwaysOn()
+ && (mState == ScrimState.AOD || mState == ScrimState.PULSING)) {
+ if (!mWallpaperVisibilityTimedOut) {
+ mTimeTicker.schedule(mDozeParameters.getWallpaperAodDuration(),
+ AlarmTimeout.MODE_IGNORE_IF_SCHEDULED);
}
+ } else {
+ mTimeTicker.cancel();
+ mWallpaperVisibilityTimedOut = false;
}
if (!mKeyguardUpdateMonitor.needsSlowUnlockTransition()) {
@@ -274,6 +309,30 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
mTracking = false;
}
+ @VisibleForTesting
+ protected void onHideWallpaperTimeout() {
+ if (mState != ScrimState.AOD && mState != ScrimState.PULSING) {
+ return;
+ }
+
+ holdWakeLock();
+ mWallpaperVisibilityTimedOut = true;
+ mAnimateChange = true;
+ mAnimationDuration = mDozeParameters.getWallpaperFadeOutDuration();
+ scheduleUpdate();
+ }
+
+ private void holdWakeLock() {
+ if (!mWakeLockHeld) {
+ if (mWakeLock != null) {
+ mWakeLockHeld = true;
+ mWakeLock.acquire();
+ } else {
+ Log.w(TAG, "Cannot hold wake lock, it has not been set yet");
+ }
+ }
+ }
+
/**
* Current state of the shade expansion when pulling it from the top.
* This value is 1 when on top of the keyguard and goes to 0 as the user drags up.
@@ -310,7 +369,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
mCurrentInFrontAlpha = 0;
}
} else {
- Log.w(TAG, "Invalid state, cannot set panel expansion when: " + mState);
return;
}
@@ -387,6 +445,14 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
mLightBarController.setScrimColor(mScrimInFront.getColors());
}
+ // We want to override the back scrim opacity for AOD and PULSING
+ // when it's time to fade the wallpaper away.
+ boolean overrideBackScrimAlpha = (mState == ScrimState.PULSING || mState == ScrimState.AOD)
+ && mWallpaperVisibilityTimedOut;
+ if (overrideBackScrimAlpha) {
+ mCurrentBehindAlpha = 1;
+ }
+
setScrimInFrontAlpha(mCurrentInFrontAlpha);
setScrimBehindAlpha(mCurrentBehindAlpha);
@@ -394,12 +460,18 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
}
private void dispatchScrimsVisible() {
- boolean scrimsVisible = mScrimBehind.getViewAlpha() > 0 || mScrimInFront.getViewAlpha() > 0;
-
- if (mScrimsVisible != scrimsVisible) {
- mScrimsVisible = scrimsVisible;
+ final int currentScrimVisibility;
+ if (mScrimInFront.getViewAlpha() == 1 || mScrimBehind.getViewAlpha() == 1) {
+ currentScrimVisibility = VISIBILITY_FULLY_OPAQUE;
+ } else if (mScrimInFront.getViewAlpha() == 0 && mScrimBehind.getViewAlpha() == 0) {
+ currentScrimVisibility = VISIBILITY_FULLY_TRANSPARENT;
+ } else {
+ currentScrimVisibility = VISIBILITY_SEMI_TRANSPARENT;
+ }
- mScrimVisibleListener.accept(scrimsVisible);
+ if (mScrimsVisibility != currentScrimVisibility) {
+ mScrimsVisibility = currentScrimVisibility;
+ mScrimVisibleListener.accept(currentScrimVisibility);
}
}
@@ -807,6 +879,14 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
pw.print(" mTracking="); pw.println(mTracking);
}
+ public void setWallpaperSupportsAmbientMode(boolean wallpaperSupportsAmbientMode) {
+ mWallpaperSupportsAmbientMode = wallpaperSupportsAmbientMode;
+ ScrimState[] states = ScrimState.values();
+ for (int i = 0; i < states.length; i++) {
+ states[i].setWallpaperSupportsAmbientMode(wallpaperSupportsAmbientMode);
+ }
+ }
+
public interface Callback {
default void onStart() {
}
diff --git a/com/android/systemui/statusbar/phone/ScrimState.java b/com/android/systemui/statusbar/phone/ScrimState.java
index 0db98f37..fa2c1b3f 100644
--- a/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/com/android/systemui/statusbar/phone/ScrimState.java
@@ -19,7 +19,9 @@ package com.android.systemui.statusbar.phone;
import android.graphics.Color;
import android.os.Trace;
+import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
/**
* Possible states of the ScrimController state machine.
@@ -38,12 +40,18 @@ public enum ScrimState {
@Override
public void prepare(ScrimState previousState) {
- // DisplayPowerManager will blank the screen, we'll just
- // set our scrim to black in this frame to avoid flickering and
- // fade it out afterwards.
- mBlankScreen = previousState == ScrimState.AOD;
+ mBlankScreen = false;
if (previousState == ScrimState.AOD) {
- updateScrimColor(mScrimInFront, 1, Color.BLACK);
+ mAnimationDuration = StackStateAnimator.ANIMATION_DURATION_WAKEUP;
+ if (mDisplayRequiresBlanking) {
+ // DisplayPowerManager will blank the screen, we'll just
+ // set our scrim to black in this frame to avoid flickering and
+ // fade it out afterwards.
+ mBlankScreen = true;
+ updateScrimColor(mScrimInFront, 1, Color.BLACK);
+ }
+ } else {
+ mAnimationDuration = ScrimController.ANIMATION_DURATION;
}
mCurrentBehindAlpha = mScrimBehindAlphaKeyguard;
mCurrentInFrontAlpha = 0;
@@ -78,18 +86,20 @@ public enum ScrimState {
AOD {
@Override
public void prepare(ScrimState previousState) {
- if (previousState == ScrimState.PULSING) {
+ if (previousState == ScrimState.PULSING && !mCanControlScreenOff) {
updateScrimColor(mScrimInFront, 1, Color.BLACK);
}
final boolean alwaysOnEnabled = mDozeParameters.getAlwaysOn();
- mBlankScreen = previousState == ScrimState.PULSING;
- mCurrentBehindAlpha = 1;
+ final boolean wasPulsing = previousState == ScrimState.PULSING;
+ mBlankScreen = wasPulsing && !mCanControlScreenOff;
+ mCurrentBehindAlpha = mWallpaperSupportsAmbientMode
+ && !mKeyguardUpdateMonitor.hasLockscreenWallpaper() ? 0f : 1f;
mCurrentInFrontAlpha = alwaysOnEnabled ? mAodFrontScrimAlpha : 1f;
mCurrentInFrontTint = Color.BLACK;
mCurrentBehindTint = Color.BLACK;
// DisplayPowerManager will blank the screen for us, we just need
// to set our state.
- mAnimateChange = false;
+ mAnimateChange = mCanControlScreenOff;
}
},
@@ -99,12 +109,15 @@ public enum ScrimState {
PULSING {
@Override
public void prepare(ScrimState previousState) {
- mCurrentBehindAlpha = 1;
mCurrentInFrontAlpha = 0;
mCurrentInFrontTint = Color.BLACK;
+ mCurrentBehindAlpha = mWallpaperSupportsAmbientMode
+ && !mKeyguardUpdateMonitor.hasLockscreenWallpaper() ? 0f : 1f;
mCurrentBehindTint = Color.BLACK;
- mBlankScreen = true;
- updateScrimColor(mScrimInFront, 1, Color.BLACK);
+ mBlankScreen = mDisplayRequiresBlanking;
+ if (mDisplayRequiresBlanking) {
+ updateScrimColor(mScrimInFront, 1, Color.BLACK);
+ }
}
},
@@ -147,11 +160,18 @@ public enum ScrimState {
ScrimView mScrimInFront;
ScrimView mScrimBehind;
DozeParameters mDozeParameters;
+ boolean mDisplayRequiresBlanking;
+ boolean mCanControlScreenOff;
+ boolean mWallpaperSupportsAmbientMode;
+ KeyguardUpdateMonitor mKeyguardUpdateMonitor;
public void init(ScrimView scrimInFront, ScrimView scrimBehind, DozeParameters dozeParameters) {
mScrimInFront = scrimInFront;
mScrimBehind = scrimBehind;
mDozeParameters = dozeParameters;
+ mDisplayRequiresBlanking = dozeParameters.getDisplayNeedsBlanking();
+ mCanControlScreenOff = dozeParameters.getCanControlScreenOffAnimation();
+ mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(scrimInFront.getContext());
}
public void prepare(ScrimState previousState) {
@@ -205,4 +225,8 @@ public enum ScrimState {
public void setScrimBehindAlphaKeyguard(float scrimBehindAlphaKeyguard) {
mScrimBehindAlphaKeyguard = scrimBehindAlphaKeyguard;
}
+
+ public void setWallpaperSupportsAmbientMode(boolean wallpaperSupportsAmbientMode) {
+ mWallpaperSupportsAmbientMode = wallpaperSupportsAmbientMode;
+ }
} \ No newline at end of file
diff --git a/com/android/systemui/statusbar/phone/StatusBar.java b/com/android/systemui/statusbar/phone/StatusBar.java
index dc8100f5..2da1e4d1 100644
--- a/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/com/android/systemui/statusbar/phone/StatusBar.java
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.phone;
-import static android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE;
import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
import static android.app.StatusBarManager.windowStateToString;
@@ -25,8 +24,10 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager
+ .NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION;
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
import static com.android.systemui.statusbar.NotificationMediaManager.DEBUG_MEDIA;
-import static com.android.systemui.statusbar.notification.NotificationInflater.InflationCallback;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
@@ -41,14 +42,15 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
+import android.app.AlarmManager;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.app.RemoteInput;
import android.app.StatusBarManager;
import android.app.TaskStackBuilder;
import android.app.WallpaperColors;
+import android.app.WallpaperInfo;
import android.app.WallpaperManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
@@ -60,14 +62,12 @@ import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.om.IOverlayManager;
import android.content.om.OverlayInfo;
-import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.PointF;
@@ -82,7 +82,6 @@ import android.media.MediaMetadata;
import android.metrics.LogMaker;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -97,18 +96,14 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.os.Vibrator;
import android.provider.Settings;
-import android.service.notification.NotificationListenerService.RankingMap;
-import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
import android.service.vr.IVrManager;
import android.service.vr.IVrStateCallbacks;
-import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
-import android.util.SparseBooleanArray;
import android.view.Display;
import android.view.IWindowManager;
import android.view.KeyEvent;
@@ -125,9 +120,7 @@ import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateInterpolator;
import android.widget.DateTimeView;
import android.widget.ImageView;
-import android.widget.RemoteViews;
import android.widget.TextView;
-import android.widget.Toast;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.colorextraction.ColorExtractor;
@@ -135,9 +128,7 @@ import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.statusbar.NotificationVisibility;
import com.android.internal.statusbar.StatusBarIcon;
-import com.android.internal.util.NotificationMessagingUtil;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.MessagingGroup;
import com.android.internal.widget.MessagingMessage;
@@ -147,13 +138,10 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.ActivityStarterDelegate;
import com.android.systemui.AutoReinflateContainer;
-import com.android.systemui.DejankUtils;
import com.android.systemui.DemoMode;
import com.android.systemui.Dependency;
import com.android.systemui.EventLogTags;
-import com.android.systemui.ForegroundServiceController;
import com.android.systemui.Interpolators;
-import com.android.systemui.OverviewProxyService;
import com.android.systemui.Prefs;
import com.android.systemui.R;
import com.android.systemui.RecentsComponent;
@@ -199,18 +187,22 @@ import com.android.systemui.statusbar.KeyboardShortcuts;
import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.NotificationData.Entry;
+import com.android.systemui.statusbar.NotificationEntryManager;
import com.android.systemui.statusbar.NotificationGutsManager;
import com.android.systemui.statusbar.NotificationInfo;
+import com.android.systemui.statusbar.NotificationListener;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.NotificationLogger;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationPresenter;
+import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.NotificationViewHierarchyManager;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.SignalClusterView;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.AboveShelfObserver;
-import com.android.systemui.statusbar.notification.InflationException;
-import com.android.systemui.statusbar.notification.RowInflaterTask;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
import com.android.systemui.statusbar.policy.BatteryController;
@@ -229,16 +221,11 @@ import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.policy.PreviewInflater;
-import com.android.systemui.statusbar.policy.RemoteInputView;
import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
-import com.android.systemui.statusbar.stack.NotificationStackScrollLayout
- .OnChildLocationsChangedListener;
import com.android.systemui.util.NotificationChannels;
-import com.android.systemui.util.leak.LeakDetector;
-import com.android.systemui.util.wakelock.WakeLock;
import com.android.systemui.volume.VolumeComponent;
import java.io.FileDescriptor;
@@ -246,28 +233,17 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Stack;
public class StatusBar extends SystemUI implements DemoMode,
DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener,
- OnHeadsUpChangedListener, VisualStabilityManager.Callback, CommandQueue.Callbacks,
- ActivatableNotificationView.OnActivatedListener,
- ExpandableNotificationRow.ExpansionLogger, NotificationData.Environment,
- ExpandableNotificationRow.OnExpandClickListener, InflationCallback,
+ OnHeadsUpChangedListener, CommandQueue.Callbacks,
ColorExtractor.OnColorsChangedListener, ConfigurationListener, NotificationPresenter {
public static final boolean MULTIUSER_DEBUG = false;
- public static final boolean ENABLE_REMOTE_INPUT =
- SystemProperties.getBoolean("debug.enable_remote_input", true);
public static final boolean ENABLE_CHILD_NOTIFICATIONS
= SystemProperties.getBoolean("debug.child_notifs", true);
- public static final boolean FORCE_REMOTE_INPUT_HISTORY =
- SystemProperties.getBoolean("debug.force_remoteinput_history", true);
- private static final boolean ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT = false;
protected static final int MSG_HIDE_RECENT_APPS = 1020;
protected static final int MSG_PRELOAD_RECENT_APPS = 1022;
@@ -275,11 +251,6 @@ public class StatusBar extends SystemUI implements DemoMode,
protected static final int MSG_TOGGLE_KEYBOARD_SHORTCUTS_MENU = 1026;
protected static final int MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU = 1027;
- protected static final boolean ENABLE_HEADS_UP = true;
- protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
-
- private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
-
// Should match the values in PhoneWindowManager
public static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
@@ -289,8 +260,6 @@ public class StatusBar extends SystemUI implements DemoMode,
"com.android.systemui.statusbar.banner_action_cancel";
private static final String BANNER_ACTION_SETUP =
"com.android.systemui.statusbar.banner_action_setup";
- private static final String NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION
- = "com.android.systemui.statusbar.work_challenge_unlocked_notification_action";
public static final String TAG = "StatusBar";
public static final boolean DEBUG = false;
public static final boolean SPEW = false;
@@ -317,15 +286,12 @@ public class StatusBar extends SystemUI implements DemoMode,
// Time after we abort the launch transition.
private static final long LAUNCH_TRANSITION_TIMEOUT_MS = 5000;
- private static final boolean CLOSE_PANEL_WHEN_EMPTIED = true;
+ protected static final boolean CLOSE_PANEL_WHEN_EMPTIED = true;
private static final int STATUS_OR_NAV_TRANSIENT =
View.STATUS_BAR_TRANSIENT | View.NAVIGATION_BAR_TRANSIENT;
private static final long AUTOHIDE_TIMEOUT_MS = 2250;
- /** The minimum delay in ms between reports of notification visibility. */
- private static final int VISIBILITY_REPORT_MIN_DELAY_MS = 500;
-
/**
* The delay to reset the hint text when the hint animation is finished running.
*/
@@ -348,14 +314,6 @@ public class StatusBar extends SystemUI implements DemoMode,
private static final boolean ENABLE_LOCKSCREEN_WALLPAPER = true;
/**
- * How long to wait before auto-dismissing a notification that was kept for remote input, and
- * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
- * these given that they technically don't exist anymore. We wait a bit in case the app issues
- * an update.
- */
- private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
-
- /**
* Never let the alpha become zero for surfaces that draw with SRC - otherwise the RenderNode
* won't draw anything and uninitialized memory will show through
* if mScrimSrcModeEnabled. Note that 0.001 is rounded down to 0 in
@@ -380,8 +338,6 @@ public class StatusBar extends SystemUI implements DemoMode,
*/
protected int mState;
protected boolean mBouncerShowing;
- protected boolean mShowLockscreenNotifications;
- protected boolean mAllowLockscreenRemoteInput;
private PhoneStatusBarPolicy mIconPolicy;
@@ -413,13 +369,6 @@ public class StatusBar extends SystemUI implements DemoMode,
protected NotificationPanelView mNotificationPanel; // the sliding/resizing panel within the notification window
private TextView mNotificationPanelDebugText;
- /**
- * {@code true} if notifications not part of a group should by default be rendered in their
- * expanded state. If {@code false}, then only the first notification will be expanded if
- * possible.
- */
- private boolean mAlwaysExpandNonGroupedNotification;
-
// settings
private QSPanel mQSPanel;
@@ -447,6 +396,9 @@ public class StatusBar extends SystemUI implements DemoMode,
private final ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>();
private NotificationGutsManager mGutsManager;
+ protected NotificationLogger mNotificationLogger;
+ protected NotificationEntryManager mEntryManager;
+ protected NotificationViewHierarchyManager mViewHierarchyManager;
// for disabling the status bar
private int mDisabled1 = 0;
@@ -498,23 +450,6 @@ public class StatusBar extends SystemUI implements DemoMode,
};
protected final H mHandler = createHandler();
- final private ContentObserver mHeadsUpObserver = new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- boolean wasUsing = mUseHeadsUp;
- mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts
- && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt(
- mContext.getContentResolver(), Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
- Settings.Global.HEADS_UP_OFF);
- Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled"));
- if (wasUsing != mUseHeadsUp) {
- if (!mUseHeadsUp) {
- Log.d(TAG, "dismissing any existing heads up notification on disable event");
- mHeadsUpManager.releaseAllImmediately();
- }
- }
- }
- };
private int mInteractingWindows;
private boolean mAutohideSuspended;
@@ -544,11 +479,25 @@ public class StatusBar extends SystemUI implements DemoMode,
new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER);
private NotificationMediaManager mMediaManager;
+ protected NotificationLockscreenUserManager mLockscreenUserManager;
+ protected NotificationRemoteInputManager mRemoteInputManager;
- /** Keys of notifications currently visible to the user. */
- private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications =
- new ArraySet<>();
- private long mLastVisibilityReportUptimeMs;
+ private BroadcastReceiver mWallpaperChangedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ WallpaperManager wallpaperManager = context.getSystemService(WallpaperManager.class);
+ if (wallpaperManager == null) {
+ Log.w(TAG, "WallpaperManager not available");
+ return;
+ }
+ WallpaperInfo info = wallpaperManager.getWallpaperInfo();
+ final boolean supportsAmbientMode = info != null &&
+ info.getSupportsAmbientMode();
+
+ mStatusBarWindowManager.setWallpaperSupportsAmbientMode(supportsAmbientMode);
+ mScrimController.setWallpaperSupportsAmbientMode(supportsAmbientMode);
+ }
+ };
private Runnable mLaunchTransitionEndRunnable;
protected boolean mLaunchTransitionFadingAway;
@@ -571,83 +520,6 @@ public class StatusBar extends SystemUI implements DemoMode,
private boolean mWereIconsJustHidden;
private boolean mBouncerWasShowingWhenHidden;
- private final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
- new OnChildLocationsChangedListener() {
- @Override
- public void onChildLocationsChanged(
- NotificationStackScrollLayout stackScrollLayout) {
- if (mHandler.hasCallbacks(mVisibilityReporter)) {
- // Visibilities will be reported when the existing
- // callback is executed.
- return;
- }
- // Calculate when we're allowed to run the visibility
- // reporter. Note that this timestamp might already have
- // passed. That's OK, the callback will just be executed
- // ASAP.
- long nextReportUptimeMs =
- mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
- mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
- }
- };
-
- // Tracks notifications currently visible in mNotificationStackScroller and
- // emits visibility events via NoMan on changes.
- protected final Runnable mVisibilityReporter = new Runnable() {
- private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications =
- new ArraySet<>();
- private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications =
- new ArraySet<>();
- private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications =
- new ArraySet<>();
-
- @Override
- public void run() {
- mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis();
-
- // 1. Loop over mNotificationData entries:
- // A. Keep list of visible notifications.
- // B. Keep list of previously hidden, now visible notifications.
- // 2. Compute no-longer visible notifications by removing currently
- // visible notifications from the set of previously visible
- // notifications.
- // 3. Report newly visible and no-longer visible notifications.
- // 4. Keep currently visible notifications for next report.
- ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
- int N = activeNotifications.size();
- for (int i = 0; i < N; i++) {
- Entry entry = activeNotifications.get(i);
- String key = entry.notification.getKey();
- boolean isVisible = mStackScroller.isInVisibleLocation(entry.row);
- NotificationVisibility visObj = NotificationVisibility.obtain(key, i, isVisible);
- boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
- if (isVisible) {
- // Build new set of visible notifications.
- mTmpCurrentlyVisibleNotifications.add(visObj);
- if (!previouslyVisible) {
- mTmpNewlyVisibleNotifications.add(visObj);
- }
- } else {
- // release object
- visObj.recycle();
- }
- }
- mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications);
- mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications);
-
- logNotificationVisibilityChanges(
- mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications);
-
- recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
- mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications);
-
- recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
- mTmpCurrentlyVisibleNotifications.clear();
- mTmpNewlyVisibleNotifications.clear();
- mTmpNoLongerVisibleNotifications.clear();
- }
- };
-
// Notifies StatusBarKeyguardViewManager every time the keyguard transition is over,
// this animation is tied to the scrim for historic reasons.
// TODO: notify when keyguard has faded away instead of the scrim.
@@ -655,25 +527,22 @@ public class StatusBar extends SystemUI implements DemoMode,
.Callback() {
@Override
public void onFinished() {
- notifyKeyguardState();
- }
-
- @Override
- public void onCancelled() {
- notifyKeyguardState();
- }
-
- private void notifyKeyguardState() {
if (mStatusBarKeyguardViewManager == null) {
Log.w(TAG, "Tried to notify keyguard visibility when "
+ "mStatusBarKeyguardViewManager was null");
return;
}
- mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+ if (mKeyguardFadingAway) {
+ mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+ }
+ }
+
+ @Override
+ public void onCancelled() {
+ onFinished();
}
};
- private NotificationMessagingUtil mMessagingUtil;
private KeyguardUserSwitcher mKeyguardUserSwitcher;
private UserSwitcherController mUserSwitcherController;
private NetworkController mNetworkController;
@@ -688,31 +557,18 @@ public class StatusBar extends SystemUI implements DemoMode,
private final LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
protected NotificationIconAreaController mNotificationIconAreaController;
private boolean mReinflateNotificationsOnUserSwitched;
- private final HashMap<String, Entry> mPendingNotifications = new HashMap<>();
private boolean mClearAllEnabled;
@Nullable private View mAmbientIndicationContainer;
private SysuiColorExtractor mColorExtractor;
- private ForegroundServiceController mForegroundServiceController;
private ScreenLifecycle mScreenLifecycle;
@VisibleForTesting WakefulnessLifecycle mWakefulnessLifecycle;
- private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
- final int N = array.size();
- for (int i = 0 ; i < N; i++) {
- array.valueAt(i).recycle();
- }
- array.clear();
- }
-
private final View.OnClickListener mGoToLockedShadeListener = v -> {
if (mState == StatusBarState.KEYGUARD) {
wakeUpIfDozing(SystemClock.uptimeMillis(), v);
goToLockedShade(null);
}
};
- private final HashMap<ExpandableNotificationRow, List<ExpandableNotificationRow>>
- mTmpChildOrderMap = new HashMap<>();
- private RankingMap mLatestRankingMap;
private boolean mNoAnimationOnNextBarModeChange;
private FalsingManager mFalsingManager;
@@ -731,6 +587,12 @@ public class StatusBar extends SystemUI implements DemoMode,
@Override
public void start() {
+ mGroupManager = Dependency.get(NotificationGroupManager.class);
+ mVisualStabilityManager = Dependency.get(VisualStabilityManager.class);
+ mNotificationLogger = Dependency.get(NotificationLogger.class);
+ mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class);
+ mNotificationListener = Dependency.get(NotificationListener.class);
+ mGroupManager = Dependency.get(NotificationGroupManager.class);
mNetworkController = Dependency.get(NetworkController.class);
mUserSwitcherController = Dependency.get(UserSwitcherController.class);
mScreenLifecycle = Dependency.get(ScreenLifecycle.class);
@@ -739,25 +601,25 @@ public class StatusBar extends SystemUI implements DemoMode,
mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
mBatteryController = Dependency.get(BatteryController.class);
mAssistManager = Dependency.get(AssistManager.class);
- mSystemServicesProxy = SystemServicesProxy.getInstance(mContext);
mOverlayManager = IOverlayManager.Stub.asInterface(
ServiceManager.getService(Context.OVERLAY_SERVICE));
+ mLockscreenUserManager = Dependency.get(NotificationLockscreenUserManager.class);
+ mGutsManager = Dependency.get(NotificationGutsManager.class);
+ mMediaManager = Dependency.get(NotificationMediaManager.class);
+ mEntryManager = Dependency.get(NotificationEntryManager.class);
+ mViewHierarchyManager = Dependency.get(NotificationViewHierarchyManager.class);
mColorExtractor = Dependency.get(SysuiColorExtractor.class);
mColorExtractor.addOnColorsChangedListener(this);
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
- mForegroundServiceController = Dependency.get(ForegroundServiceController.class);
-
mDisplay = mWindowManager.getDefaultDisplay();
updateDisplaySize();
Resources res = mContext.getResources();
mScrimSrcModeEnabled = res.getBoolean(R.bool.config_status_bar_scrim_behind_use_src);
mClearAllEnabled = res.getBoolean(R.bool.config_enableNotificationsClearAll);
- mAlwaysExpandNonGroupedNotification =
- res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
DateTimeView.setReceiverHandler(Dependency.get(Dependency.TIME_TICK_HANDLER));
putComponent(StatusBar.class, this);
@@ -767,47 +629,22 @@ public class StatusBar extends SystemUI implements DemoMode,
mDevicePolicyManager = (DevicePolicyManager) mContext.getSystemService(
Context.DEVICE_POLICY_SERVICE);
- mNotificationData = new NotificationData(this);
- mMessagingUtil = new NotificationMessagingUtil(mContext);
-
mAccessibilityManager = (AccessibilityManager)
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class);
- mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
- mContext.getContentResolver().registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.ZEN_MODE), false,
- mSettingsObserver);
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS), false,
- mLockscreenSettingsObserver,
- UserHandle.USER_ALL);
- if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT),
- false,
- mSettingsObserver,
- UserHandle.USER_ALL);
- }
-
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS),
- true,
- mLockscreenSettingsObserver,
- UserHandle.USER_ALL);
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
mRecents = getComponent(Recents.class);
- mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
mLockPatternUtils = new LockPatternUtils(mContext);
- mMediaManager = new NotificationMediaManager(this, mContext);
+ mMediaManager.setUpWithPresenter(this, mEntryManager);
// Connect in to the status bar manager service
mCommandQueue = getComponent(CommandQueue.class);
@@ -828,7 +665,12 @@ public class StatusBar extends SystemUI implements DemoMode,
createAndAddWindows();
- mSettingsObserver.onChange(false); // set up
+ // Make sure we always have the most current wallpaper info.
+ IntentFilter wallpaperChangedFilter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED);
+ mContext.registerReceiver(mWallpaperChangedReceiver, wallpaperChangedFilter);
+ mWallpaperChangedReceiver.onReceive(mContext, null);
+
+ mLockscreenUserManager.setUpWithPresenter(this, mEntryManager);
mCommandQueue.disable(switches[0], switches[6], false /* animate */);
setSystemUiVisibility(switches[1], switches[7], switches[8], 0xffffffff,
fullscreenStackBounds, dockedStackBounds);
@@ -843,14 +685,7 @@ public class StatusBar extends SystemUI implements DemoMode,
}
// Set up the initial notification state.
- try {
- mNotificationListener.registerAsSystemService(mContext,
- new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()),
- UserHandle.USER_ALL);
- } catch (RemoteException e) {
- Log.e(TAG, "Unable to register notification listener", e);
- }
-
+ mNotificationListener.setUpWithPresenter(this, mEntryManager);
if (DEBUG) {
Log.d(TAG, String.format(
@@ -863,29 +698,13 @@ public class StatusBar extends SystemUI implements DemoMode,
));
}
- mCurrentUserId = ActivityManager.getCurrentUser();
- setHeadsUpUser(mCurrentUserId);
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_USER_SWITCHED);
- filter.addAction(Intent.ACTION_USER_ADDED);
- filter.addAction(Intent.ACTION_USER_PRESENT);
- filter.addAction(Intent.ACTION_USER_UNLOCKED);
- mContext.registerReceiver(mBaseBroadcastReceiver, filter);
+ setHeadsUpUser(mLockscreenUserManager.getCurrentUserId());
IntentFilter internalFilter = new IntentFilter();
- internalFilter.addAction(NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION);
internalFilter.addAction(BANNER_ACTION_CANCEL);
internalFilter.addAction(BANNER_ACTION_SETUP);
- mContext.registerReceiver(mBaseBroadcastReceiver, internalFilter, PERMISSION_SELF, null);
-
- IntentFilter allUsersFilter = new IntentFilter();
- allUsersFilter.addAction(
- DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
- allUsersFilter.addAction(Intent.ACTION_DEVICE_LOCKED_CHANGED);
- mContext.registerReceiverAsUser(mAllUsersReceiver, UserHandle.ALL, allUsersFilter,
- null, null);
- updateCurrentProfilesCache();
+ mContext.registerReceiver(mBannerActionBroadcastReceiver, internalFilter, PERMISSION_SELF,
+ null);
IVrManager vrManager = IVrManager.Stub.asInterface(ServiceManager.getService(
Context.VR_SERVICE));
@@ -899,17 +718,7 @@ public class StatusBar extends SystemUI implements DemoMode,
// Lastly, call to the icon policy to install/update all the icons.
mIconPolicy = new PhoneStatusBarPolicy(mContext, mIconController);
- mSettingsObserver.onChange(false); // set up
- mHeadsUpObserver.onChange(true); // set up
- if (ENABLE_HEADS_UP) {
- mContext.getContentResolver().registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), true,
- mHeadsUpObserver);
- mContext.getContentResolver().registerContentObserver(
- Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
- mHeadsUpObserver);
- }
mUnlockMethodCache = UnlockMethodCache.getInstance(mContext);
mUnlockMethodCache.addListener(this);
startKeyguard();
@@ -944,8 +753,7 @@ public class StatusBar extends SystemUI implements DemoMode,
// into fragments, but the rest here, it leaves some awkward lifecycle and whatnot.
mNotificationPanel = mStatusBarWindow.findViewById(R.id.notification_panel);
mStackScroller = mStatusBarWindow.findViewById(R.id.notification_stack_scroller);
- mGutsManager = new NotificationGutsManager(this, mStackScroller,
- mCheckSaveListener, mContext,
+ mGutsManager.setUpWithPresenter(this, mEntryManager, mStackScroller, mCheckSaveListener,
key -> {
try {
mBarService.onNotificationSettingsViewed(key);
@@ -953,6 +761,7 @@ public class StatusBar extends SystemUI implements DemoMode,
// if we're here we're dead
}
});
+ mNotificationLogger.setUpWithEntryManager(mEntryManager, mStackScroller);
mNotificationPanel.setStatusBar(this);
mNotificationPanel.setGroupManager(mGroupManager);
mAboveShelfObserver = new AboveShelfObserver(mStackScroller);
@@ -991,11 +800,13 @@ public class StatusBar extends SystemUI implements DemoMode,
mHeadsUpManager.addListener(mGroupManager);
mHeadsUpManager.addListener(mVisualStabilityManager);
mNotificationPanel.setHeadsUpManager(mHeadsUpManager);
- mNotificationData.setHeadsUpManager(mHeadsUpManager);
mGroupManager.setHeadsUpManager(mHeadsUpManager);
mHeadsUpManager.setVisualStabilityManager(mVisualStabilityManager);
putComponent(HeadsUpManager.class, mHeadsUpManager);
+ mEntryManager.setUpWithPresenter(this, mStackScroller, this, mHeadsUpManager);
+ mViewHierarchyManager.setUpWithPresenter(this, mEntryManager, mStackScroller);
+
if (MULTIUSER_DEBUG) {
mNotificationPanelDebugText = mNotificationPanel.findViewById(R.id.header_debug_info);
mNotificationPanelDebugText.setVisibility(View.VISIBLE);
@@ -1011,7 +822,7 @@ public class StatusBar extends SystemUI implements DemoMode,
// no window manager? good luck with that
}
- mStackScroller.setLongPressListener(getNotificationLongClicker());
+ mStackScroller.setLongPressListener(mEntryManager.getNotificationLongClicker());
mStackScroller.setStatusBar(this);
mStackScroller.setGroupManager(mGroupManager);
mStackScroller.setHeadsUpManager(mHeadsUpManager);
@@ -1070,9 +881,9 @@ public class StatusBar extends SystemUI implements DemoMode,
scrimBehind, scrimInFront, headsUpScrim, mLockscreenWallpaper,
scrimsVisible -> {
if (mStatusBarWindowManager != null) {
- mStatusBarWindowManager.setScrimsVisible(scrimsVisible);
+ mStatusBarWindowManager.setScrimsVisibility(scrimsVisible);
}
- }, new DozeParameters(mContext));
+ }, new DozeParameters(mContext), mContext.getSystemService(AlarmManager.class));
if (mScrimSrcModeEnabled) {
Runnable runnable = () -> {
boolean asSrc = mBackdrop.getVisibility() != View.VISIBLE;
@@ -1209,7 +1020,7 @@ public class StatusBar extends SystemUI implements DemoMode,
protected View.OnTouchListener getStatusBarWindowTouchListener() {
return (v, event) -> {
checkUserAutohide(event);
- checkRemoteInputOutside(event);
+ mRemoteInputManager.checkRemoteInputOutside(event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (mExpandedVisible) {
animateCollapsePanels();
@@ -1234,7 +1045,7 @@ public class StatusBar extends SystemUI implements DemoMode,
MessagingGroup.dropCache();
// start old BaseStatusBar.onDensityOrFontScaleChanged().
if (!KeyguardUpdateMonitor.getInstance(mContext).isSwitchingUser()) {
- updateNotificationsOnDensityOrFontScaleChanged();
+ mEntryManager.updateNotificationsOnDensityOrFontScaleChanged();
} else {
mReinflateNotificationsOnUserSwitched = true;
}
@@ -1298,20 +1109,6 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
- private void updateNotificationsOnDensityOrFontScaleChanged() {
- ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
- for (int i = 0; i < activeNotifications.size(); i++) {
- Entry entry = activeNotifications.get(i);
- boolean exposedGuts = mGutsManager.getExposedGuts() != null
- && entry.row.getGuts() == mGutsManager.getExposedGuts();
- entry.row.onDensityOrFontScaleChanged();
- if (exposedGuts) {
- mGutsManager.setExposedGuts(entry.row.getGuts());
- mGutsManager.bindGuts(entry.row);
- }
- }
- }
-
private void inflateSignalClusters() {
if (mKeyguardStatusBar != null) reinflateSignalCluster(mKeyguardStatusBar);
}
@@ -1425,13 +1222,13 @@ public class StatusBar extends SystemUI implements DemoMode,
mStackScroller.setDismissAllInProgress(false);
for (ExpandableNotificationRow rowToRemove : viewsToRemove) {
if (mStackScroller.canChildBeDismissed(rowToRemove)) {
- removeNotification(rowToRemove.getEntry().key, null);
+ mEntryManager.removeNotification(rowToRemove.getEntry().key, null);
} else {
rowToRemove.resetTranslation();
}
}
try {
- mBarService.onClearAllNotifications(mCurrentUserId);
+ mBarService.onClearAllNotifications(mLockscreenUserManager.getCurrentUserId());
} catch (Exception ex) {
}
});
@@ -1471,15 +1268,6 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
- protected void setZenMode(int mode) {
- // start old BaseStatusBar.setZenMode().
- if (isDeviceProvisioned()) {
- mZenMode = mode;
- updateNotifications();
- }
- // end old BaseStatusBar.setZenMode().
- }
-
protected void startKeyguard() {
Trace.beginSection("StatusBar#startKeyguard");
KeyguardViewMediator keyguardViewMediator = getComponent(KeyguardViewMediator.class);
@@ -1491,31 +1279,7 @@ public class StatusBar extends SystemUI implements DemoMode,
mKeyguardIndicationController
.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
mFingerprintUnlockController.setStatusBarKeyguardViewManager(mStatusBarKeyguardViewManager);
- mRemoteInputController.addCallback(mStatusBarKeyguardViewManager);
-
- mRemoteInputController.addCallback(new RemoteInputController.Callback() {
- @Override
- public void onRemoteInputSent(Entry entry) {
- if (FORCE_REMOTE_INPUT_HISTORY && mKeysKeptForRemoteInput.contains(entry.key)) {
- removeNotification(entry.key, null);
- } else if (mRemoteInputEntriesToRemoveOnCollapse.contains(entry)) {
- // We're currently holding onto this notification, but from the apps point of
- // view it is already canceled, so we'll need to cancel it on the apps behalf
- // after sending - unless the app posts an update in the mean time, so wait a
- // bit.
- mHandler.postDelayed(() -> {
- if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) {
- removeNotification(entry.key, null);
- }
- }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
- }
- try {
- mBarService.onNotificationDirectReplied(entry.key);
- } catch (RemoteException e) {
- // system process is dead if we're here.
- }
- }
- });
+ mRemoteInputManager.getController().addCallback(mStatusBarKeyguardViewManager);
mKeyguardViewMediatorCallback = keyguardViewMediator.getViewMediatorCallback();
mLightBarController.setFingerprintUnlockController(mFingerprintUnlockController);
@@ -1566,207 +1330,53 @@ public class StatusBar extends SystemUI implements DemoMode,
return true;
}
- void awakenDreams() {
- SystemServicesProxy.getInstance(mContext).awakenDreamsAsync();
+ @Override
+ public void onPerformRemoveNotification(StatusBarNotification n) {
+ if (mStackScroller.hasPulsingNotifications() && mHeadsUpManager.getAllEntries().isEmpty()) {
+ // We were showing a pulse for a notification, but no notifications are pulsing anymore.
+ // Finish the pulse.
+ mDozeScrimController.pulseOutNow();
+ }
}
- public void addNotification(StatusBarNotification notification, RankingMap ranking)
- throws InflationException {
- String key = notification.getKey();
- if (DEBUG) Log.d(TAG, "addNotification key=" + key);
-
- mNotificationData.updateRanking(ranking);
- Entry shadeEntry = createNotificationViews(notification);
- boolean isHeadsUped = shouldPeek(shadeEntry);
- if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) {
- if (shouldSuppressFullScreenIntent(key)) {
- if (DEBUG) {
- Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + key);
- }
- } else if (mNotificationData.getImportance(key)
- < NotificationManager.IMPORTANCE_HIGH) {
- if (DEBUG) {
- Log.d(TAG, "No Fullscreen intent: not important enough: "
- + key);
- }
- } else {
- // Stop screensaver if the notification has a full-screen intent.
- // (like an incoming phone call)
- awakenDreams();
+ @Override
+ public void updateNotificationViews() {
+ // The function updateRowStates depends on both of these being non-null, so check them here.
+ // We may be called before they are set from DeviceProvisionedController's callback.
+ if (mStackScroller == null || mScrimController == null) return;
- // not immersive & a full-screen alert should be shown
- if (DEBUG)
- Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent");
- try {
- EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION,
- key);
- notification.getNotification().fullScreenIntent.send();
- shadeEntry.notifyFullScreenIntentLaunched();
- mMetricsLogger.count("note_fullscreen", 1);
- } catch (PendingIntent.CanceledException e) {
- }
- }
+ // Do not modify the notifications during collapse.
+ if (isCollapsing()) {
+ addPostCollapseAction(this::updateNotificationViews);
+ return;
}
- abortExistingInflation(key);
- mForegroundServiceController.addNotification(notification,
- mNotificationData.getImportance(key));
+ mViewHierarchyManager.updateNotificationViews();
- mPendingNotifications.put(key, shadeEntry);
- }
+ updateSpeedBumpIndex();
+ updateClearAll();
+ updateEmptyShadeView();
- private void abortExistingInflation(String key) {
- if (mPendingNotifications.containsKey(key)) {
- Entry entry = mPendingNotifications.get(key);
- entry.abortTask();
- mPendingNotifications.remove(key);
- }
- Entry addedEntry = mNotificationData.get(key);
- if (addedEntry != null) {
- addedEntry.abortTask();
- }
- }
+ updateQsExpansionEnabled();
- private void addEntry(Entry shadeEntry) {
- boolean isHeadsUped = shouldPeek(shadeEntry);
- if (isHeadsUped) {
- mHeadsUpManager.showNotification(shadeEntry);
- // Mark as seen immediately
- setNotificationShown(shadeEntry.notification);
- }
- addNotificationViews(shadeEntry);
- // Recalculate the position of the sliding windows and the titles.
- setAreThereNotifications();
+ // Let's also update the icons
+ mNotificationIconAreaController.updateNotificationIcons(
+ mEntryManager.getNotificationData());
}
@Override
- public void handleInflationException(StatusBarNotification notification, Exception e) {
- handleNotificationError(notification, e.getMessage());
+ public void onNotificationAdded(Entry shadeEntry) {
+ // Recalculate the position of the sliding windows and the titles.
+ setAreThereNotifications();
}
@Override
- public void onAsyncInflationFinished(Entry entry) {
- mPendingNotifications.remove(entry.key);
- // If there was an async task started after the removal, we don't want to add it back to
- // the list, otherwise we might get leaks.
- boolean isNew = mNotificationData.get(entry.key) == null;
- if (isNew && !entry.row.isRemoved()) {
- addEntry(entry);
- } else if (!isNew && entry.row.hasLowPriorityStateUpdated()) {
- mVisualStabilityManager.onLowPriorityUpdated(entry);
- updateNotificationShade();
- }
- entry.row.setLowPriorityStateUpdated(false);
- }
-
- private boolean shouldSuppressFullScreenIntent(String key) {
- if (isDeviceInVrMode()) {
- return true;
- }
-
- if (mPowerManager.isInteractive()) {
- return mNotificationData.shouldSuppressScreenOn(key);
- } else {
- return mNotificationData.shouldSuppressScreenOff(key);
- }
- }
-
- protected void updateNotificationRanking(RankingMap ranking) {
- mNotificationData.updateRanking(ranking);
- updateNotifications();
+ public void onNotificationUpdated(StatusBarNotification notification) {
+ setAreThereNotifications();
}
@Override
- public void removeNotification(String key, RankingMap ranking) {
- boolean deferRemoval = false;
- abortExistingInflation(key);
- if (mHeadsUpManager.isHeadsUp(key)) {
- // A cancel() in response to a remote input shouldn't be delayed, as it makes the
- // sending look longer than it takes.
- // Also we should not defer the removal if reordering isn't allowed since otherwise
- // some notifications can't disappear before the panel is closed.
- boolean ignoreEarliestRemovalTime = mRemoteInputController.isSpinning(key)
- && !FORCE_REMOTE_INPUT_HISTORY
- || !mVisualStabilityManager.isReorderingAllowed();
- deferRemoval = !mHeadsUpManager.removeNotification(key, ignoreEarliestRemovalTime);
- }
- mMediaManager.onNotificationRemoved(key);
-
- Entry entry = mNotificationData.get(key);
- if (FORCE_REMOTE_INPUT_HISTORY && mRemoteInputController.isSpinning(key)
- && entry.row != null && !entry.row.isDismissed()) {
- StatusBarNotification sbn = entry.notification;
-
- Notification.Builder b = Notification.Builder
- .recoverBuilder(mContext, sbn.getNotification().clone());
- CharSequence[] oldHistory = sbn.getNotification().extras
- .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
- CharSequence[] newHistory;
- if (oldHistory == null) {
- newHistory = new CharSequence[1];
- } else {
- newHistory = new CharSequence[oldHistory.length + 1];
- System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
- }
- newHistory[0] = String.valueOf(entry.remoteInputText);
- b.setRemoteInputHistory(newHistory);
-
- Notification newNotification = b.build();
-
- // Undo any compatibility view inflation
- newNotification.contentView = sbn.getNotification().contentView;
- newNotification.bigContentView = sbn.getNotification().bigContentView;
- newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
-
- StatusBarNotification newSbn = new StatusBarNotification(sbn.getPackageName(),
- sbn.getOpPkg(),
- sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(),
- newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime());
- boolean updated = false;
- try {
- updateNotification(newSbn, null);
- updated = true;
- } catch (InflationException e) {
- deferRemoval = false;
- }
- if (updated) {
- Log.w(TAG, "Keeping notification around after sending remote input "+ entry.key);
- mKeysKeptForRemoteInput.add(entry.key);
- return;
- }
- }
- if (deferRemoval) {
- mLatestRankingMap = ranking;
- mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
- return;
- }
-
- if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
- && (entry.row != null && !entry.row.isDismissed())) {
- mLatestRankingMap = ranking;
- mRemoteInputEntriesToRemoveOnCollapse.add(entry);
- return;
- }
- if (entry != null && mGutsManager.getExposedGuts() != null
- && mGutsManager.getExposedGuts() == entry.row.getGuts()
- && entry.row.getGuts() != null && !entry.row.getGuts().isLeavebehind()) {
- Log.w(TAG, "Keeping notification because it's showing guts. " + key);
- mLatestRankingMap = ranking;
- mGutsManager.setKeyToRemoveOnGutsClosed(key);
- return;
- }
-
- if (entry != null) {
- mForegroundServiceController.removeNotification(entry.notification);
- }
-
- if (entry != null && entry.row != null) {
- entry.row.setRemoved();
- mStackScroller.cleanUpViewState(entry.row);
- }
- // Let's remove the children if this was a summary
- handleGroupSummaryRemoved(key);
- StatusBarNotification old = removeNotificationViews(key, ranking);
+ public void onNotificationRemoved(String key, StatusBarNotification old) {
if (SPEW) Log.d(TAG, "removeNotification key=" + key + " old=" + old);
if (old != null) {
@@ -1783,212 +1393,6 @@ public class StatusBar extends SystemUI implements DemoMode,
}
/**
- * Ensures that the group children are cancelled immediately when the group summary is cancelled
- * instead of waiting for the notification manager to send all cancels. Otherwise this could
- * lead to flickers.
- *
- * This also ensures that the animation looks nice and only consists of a single disappear
- * animation instead of multiple.
- * @param key the key of the notification was removed
- *
- */
- private void handleGroupSummaryRemoved(String key) {
- Entry entry = mNotificationData.get(key);
- if (entry != null && entry.row != null
- && entry.row.isSummaryWithChildren()) {
- if (entry.notification.getOverrideGroupKey() != null && !entry.row.isDismissed()) {
- // We don't want to remove children for autobundled notifications as they are not
- // always cancelled. We only remove them if they were dismissed by the user.
- return;
- }
- List<ExpandableNotificationRow> notificationChildren =
- entry.row.getNotificationChildren();
- for (int i = 0; i < notificationChildren.size(); i++) {
- ExpandableNotificationRow row = notificationChildren.get(i);
- if ((row.getStatusBarNotification().getNotification().flags
- & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
- // the child is a foreground service notification which we can't remove!
- continue;
- }
- row.setKeepInParent(true);
- // we need to set this state earlier as otherwise we might generate some weird
- // animations
- row.setRemoved();
- }
- }
- }
-
- protected void performRemoveNotification(StatusBarNotification n) {
- Entry entry = mNotificationData.get(n.getKey());
- if (mRemoteInputController.isRemoteInputActive(entry)) {
- mRemoteInputController.removeRemoteInput(entry, null);
- }
- // start old BaseStatusBar.performRemoveNotification.
- final String pkg = n.getPackageName();
- final String tag = n.getTag();
- final int id = n.getId();
- final int userId = n.getUserId();
- try {
- int dismissalSurface = NotificationStats.DISMISSAL_SHADE;
- if (isHeadsUp(n.getKey())) {
- dismissalSurface = NotificationStats.DISMISSAL_PEEK;
- } else if (mStackScroller.hasPulsingNotifications()) {
- dismissalSurface = NotificationStats.DISMISSAL_AOD;
- }
- mBarService.onNotificationClear(pkg, tag, id, userId, n.getKey(),
- dismissalSurface);
- if (FORCE_REMOTE_INPUT_HISTORY
- && mKeysKeptForRemoteInput.contains(n.getKey())) {
- mKeysKeptForRemoteInput.remove(n.getKey());
- }
- removeNotification(n.getKey(), null);
-
- } catch (RemoteException ex) {
- // system process is dead if we're here.
- }
- if (mStackScroller.hasPulsingNotifications() && mHeadsUpManager.getAllEntries().isEmpty()) {
- // We were showing a pulse for a notification, but no notifications are pulsing anymore.
- // Finish the pulse.
- mDozeScrimController.pulseOutNow();
- }
- // end old BaseStatusBar.performRemoveNotification.
- }
-
- private void updateNotificationShade() {
- if (mStackScroller == null) return;
-
- // Do not modify the notifications during collapse.
- if (isCollapsing()) {
- addPostCollapseAction(this::updateNotificationShade);
- return;
- }
-
- ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
- ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
- final int N = activeNotifications.size();
- for (int i = 0; i < N; i++) {
- Entry ent = activeNotifications.get(i);
- if (ent.row.isDismissed() || ent.row.isRemoved()) {
- // we don't want to update removed notifications because they could
- // temporarily become children if they were isolated before.
- continue;
- }
- int userId = ent.notification.getUserId();
-
- // Display public version of the notification if we need to redact.
- boolean devicePublic = isLockscreenPublicMode(mCurrentUserId);
- boolean userPublic = devicePublic || isLockscreenPublicMode(userId);
- boolean needsRedaction = needsRedaction(ent);
- boolean sensitive = userPublic && needsRedaction;
- boolean deviceSensitive = devicePublic
- && !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
- ent.row.setSensitive(sensitive, deviceSensitive);
- ent.row.setNeedsRedaction(needsRedaction);
- if (mGroupManager.isChildInGroupWithSummary(ent.row.getStatusBarNotification())) {
- ExpandableNotificationRow summary = mGroupManager.getGroupSummary(
- ent.row.getStatusBarNotification());
- List<ExpandableNotificationRow> orderedChildren =
- mTmpChildOrderMap.get(summary);
- if (orderedChildren == null) {
- orderedChildren = new ArrayList<>();
- mTmpChildOrderMap.put(summary, orderedChildren);
- }
- orderedChildren.add(ent.row);
- } else {
- toShow.add(ent.row);
- }
-
- }
-
- ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
- for (int i=0; i< mStackScroller.getChildCount(); i++) {
- View child = mStackScroller.getChildAt(i);
- if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
- toRemove.add((ExpandableNotificationRow) child);
- }
- }
-
- for (ExpandableNotificationRow remove : toRemove) {
- if (mGroupManager.isChildInGroupWithSummary(remove.getStatusBarNotification())) {
- // we are only transferring this notification to its parent, don't generate an
- // animation
- mStackScroller.setChildTransferInProgress(true);
- }
- if (remove.isSummaryWithChildren()) {
- remove.removeAllChildren();
- }
- mStackScroller.removeView(remove);
- mStackScroller.setChildTransferInProgress(false);
- }
-
- removeNotificationChildren();
-
- for (int i = 0; i < toShow.size(); i++) {
- View v = toShow.get(i);
- if (v.getParent() == null) {
- mVisualStabilityManager.notifyViewAddition(v);
- mStackScroller.addView(v);
- }
- }
-
- addNotificationChildrenAndSort();
-
- // So after all this work notifications still aren't sorted correctly.
- // Let's do that now by advancing through toShow and mStackScroller in
- // lock-step, making sure mStackScroller matches what we see in toShow.
- int j = 0;
- for (int i = 0; i < mStackScroller.getChildCount(); i++) {
- View child = mStackScroller.getChildAt(i);
- if (!(child instanceof ExpandableNotificationRow)) {
- // We don't care about non-notification views.
- continue;
- }
-
- ExpandableNotificationRow targetChild = toShow.get(j);
- if (child != targetChild) {
- // Oops, wrong notification at this position. Put the right one
- // here and advance both lists.
- if (mVisualStabilityManager.canReorderNotification(targetChild)) {
- mStackScroller.changeViewPosition(targetChild, i);
- } else {
- mVisualStabilityManager.addReorderingAllowedCallback(this);
- }
- }
- j++;
-
- }
-
- mVisualStabilityManager.onReorderingFinished();
- // clear the map again for the next usage
- mTmpChildOrderMap.clear();
-
- updateRowStates();
- updateSpeedBumpIndex();
- updateClearAll();
- updateEmptyShadeView();
-
- updateQsExpansionEnabled();
-
- // Let's also update the icons
- mNotificationIconAreaController.updateNotificationIcons(mNotificationData);
- }
-
- /** @return true if the entry needs redaction when on the lockscreen. */
- private boolean needsRedaction(Entry ent) {
- int userId = ent.notification.getUserId();
-
- boolean currentUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
- boolean notiUserWantsRedaction = !userAllowsPrivateNotificationsInPublic(userId);
- boolean redactedLockscreen = currentUserWantsRedaction || notiUserWantsRedaction;
-
- boolean notificationRequestsRedaction =
- ent.notification.getNotification().visibility == Notification.VISIBILITY_PRIVATE;
- boolean userForcesRedaction = packageHasVisibilityOverride(ent.notification.getKey());
-
- return userForcesRedaction || notificationRequestsRedaction && redactedLockscreen;
- }
-
- /**
* Disable QS if device not provisioned.
* If the user switcher is simple then disable QS during setup because
* the user intends to use the lock screen user switcher, QS in not needed.
@@ -2003,81 +1407,6 @@ public class StatusBar extends SystemUI implements DemoMode,
&& !ONLY_CORE_APPS);
}
- private void addNotificationChildrenAndSort() {
- // Let's now add all notification children which are missing
- boolean orderChanged = false;
- for (int i = 0; i < mStackScroller.getChildCount(); i++) {
- View view = mStackScroller.getChildAt(i);
- if (!(view instanceof ExpandableNotificationRow)) {
- // We don't care about non-notification views.
- continue;
- }
-
- ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
- List<ExpandableNotificationRow> children = parent.getNotificationChildren();
- List<ExpandableNotificationRow> orderedChildren = mTmpChildOrderMap.get(parent);
-
- for (int childIndex = 0; orderedChildren != null && childIndex < orderedChildren.size();
- childIndex++) {
- ExpandableNotificationRow childView = orderedChildren.get(childIndex);
- if (children == null || !children.contains(childView)) {
- if (childView.getParent() != null) {
- Log.wtf(TAG, "trying to add a notification child that already has " +
- "a parent. class:" + childView.getParent().getClass() +
- "\n child: " + childView);
- // This shouldn't happen. We can recover by removing it though.
- ((ViewGroup) childView.getParent()).removeView(childView);
- }
- mVisualStabilityManager.notifyViewAddition(childView);
- parent.addChildNotification(childView, childIndex);
- mStackScroller.notifyGroupChildAdded(childView);
- }
- }
-
- // Finally after removing and adding has been performed we can apply the order.
- orderChanged |= parent.applyChildOrder(orderedChildren, mVisualStabilityManager, this);
- }
- if (orderChanged) {
- mStackScroller.generateChildOrderChangedEvent();
- }
- }
-
- private void removeNotificationChildren() {
- // First let's remove all children which don't belong in the parents
- ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
- for (int i = 0; i < mStackScroller.getChildCount(); i++) {
- View view = mStackScroller.getChildAt(i);
- if (!(view instanceof ExpandableNotificationRow)) {
- // We don't care about non-notification views.
- continue;
- }
-
- ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
- List<ExpandableNotificationRow> children = parent.getNotificationChildren();
- List<ExpandableNotificationRow> orderedChildren = mTmpChildOrderMap.get(parent);
-
- if (children != null) {
- toRemove.clear();
- for (ExpandableNotificationRow childRow : children) {
- if ((orderedChildren == null
- || !orderedChildren.contains(childRow))
- && !childRow.keepInParent()) {
- toRemove.add(childRow);
- }
- }
- for (ExpandableNotificationRow remove : toRemove) {
- parent.removeChildNotification(remove);
- if (mNotificationData.get(remove.getStatusBarNotification().getKey()) == null) {
- // We only want to add an animation if the view is completely removed
- // otherwise it's just a transfer
- mStackScroller.notifyGroupChildRemoved(remove,
- parent.getChildrenContainer());
- }
- }
- }
- }
- }
-
public void addQsTile(ComponentName tile) {
mQSPanel.getHost().addTile(tile);
}
@@ -2090,9 +1419,6 @@ public class StatusBar extends SystemUI implements DemoMode,
mQSPanel.clickTile(tile);
}
- private boolean packageHasVisibilityOverride(String key) {
- return mNotificationData.getVisibilityOverride(key) == Notification.VISIBILITY_PRIVATE;
- }
private void updateClearAll() {
if (!mClearAllEnabled) {
@@ -2123,7 +1449,7 @@ public class StatusBar extends SystemUI implements DemoMode,
private void updateEmptyShadeView() {
boolean showEmptyShadeView =
mState != StatusBarState.KEYGUARD &&
- mNotificationData.getActiveNotifications().size() == 0;
+ mEntryManager.getNotificationData().getActiveNotifications().size() == 0;
mNotificationPanel.showEmptyShadeView(showEmptyShadeView);
}
@@ -2138,7 +1464,8 @@ public class StatusBar extends SystemUI implements DemoMode,
}
ExpandableNotificationRow row = (ExpandableNotificationRow) view;
currentIndex++;
- if (!mNotificationData.isAmbient(row.getStatusBarNotification().getKey())) {
+ if (!mEntryManager.getNotificationData().isAmbient(
+ row.getStatusBarNotification().getKey())) {
speedBumpIndex = currentIndex;
}
}
@@ -2150,15 +1477,9 @@ public class StatusBar extends SystemUI implements DemoMode,
return entry.row.getParent() instanceof NotificationStackScrollLayout;
}
- @Override
- public void updateNotifications() {
- mNotificationData.filterAndSort();
-
- updateNotificationShade();
- }
public void requestNotificationUpdate() {
- updateNotifications();
+ mEntryManager.updateNotifications();
}
protected void setAreThereNotifications() {
@@ -2167,7 +1488,7 @@ public class StatusBar extends SystemUI implements DemoMode,
final boolean clearable = hasActiveNotifications() &&
hasActiveClearableNotifications();
Log.d(TAG, "setAreThereNotifications: N=" +
- mNotificationData.getActiveNotifications().size() + " any=" +
+ mEntryManager.getNotificationData().getActiveNotifications().size() + " any=" +
hasActiveNotifications() + " clearable=" + clearable);
}
@@ -2452,9 +1773,8 @@ public class StatusBar extends SystemUI implements DemoMode,
}
if ((diff1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) {
- mDisableNotificationAlerts =
- (state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0;
- mHeadsUpObserver.onChange(true);
+ mEntryManager.setDisableNotificationAlerts(
+ (state1 & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0);
}
if ((diff2 & StatusBarManager.DISABLE2_QUICK_SETTINGS) != 0) {
@@ -2517,10 +1837,40 @@ public class StatusBar extends SystemUI implements DemoMode,
return getBarState() == StatusBarState.KEYGUARD;
}
+ @Override
public boolean isDozing() {
return mDozing;
}
+ @Override
+ public boolean shouldPeek(Entry entry, StatusBarNotification sbn) {
+ if (mIsOccluded && !isDozing()) {
+ boolean devicePublic = mLockscreenUserManager.
+ isLockscreenPublicMode(mLockscreenUserManager.getCurrentUserId());
+ boolean userPublic = devicePublic
+ || mLockscreenUserManager.isLockscreenPublicMode(sbn.getUserId());
+ boolean needsRedaction = mLockscreenUserManager.needsRedaction(entry);
+ if (userPublic && needsRedaction) {
+ return false;
+ }
+ }
+
+ if (sbn.getNotification().fullScreenIntent != null) {
+ if (mAccessibilityManager.isTouchExplorationEnabled()) {
+ if (DEBUG) Log.d(TAG, "No peeking: accessible fullscreen: " + sbn.getKey());
+ return false;
+ } else if (isDozing()) {
+ // We never want heads up when we are dozing.
+ return false;
+ } else {
+ // we only allow head-up on the lockscreen if it doesn't have a fullscreen intent
+ return !mStatusBarKeyguardViewManager.isShowing()
+ || mStatusBarKeyguardViewManager.isOccluded();
+ }
+ }
+ return true;
+ }
+
@Override // NotificationData.Environment
public String getCurrentMediaNotificationKey() {
return mMediaManager.getMediaNotificationKey();
@@ -2572,7 +1922,7 @@ public class StatusBar extends SystemUI implements DemoMode,
mStatusBarWindowManager.setHeadsUpShowing(false);
mHeadsUpManager.setHeadsUpGoingAway(false);
}
- removeRemoteInputEntriesKeptUntilCollapsed();
+ mRemoteInputManager.removeRemoteInputEntriesKeptUntilCollapsed();
});
}
}
@@ -2589,34 +1939,10 @@ public class StatusBar extends SystemUI implements DemoMode,
@Override
public void onHeadsUpStateChanged(Entry entry, boolean isHeadsUp) {
- if (!isHeadsUp && mHeadsUpEntriesToRemoveOnSwitch.contains(entry)) {
- removeNotification(entry.key, mLatestRankingMap);
- mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
- if (mHeadsUpEntriesToRemoveOnSwitch.isEmpty()) {
- mLatestRankingMap = null;
- }
- } else {
- updateNotificationRanking(null);
- if (isHeadsUp) {
- mDozeServiceHost.fireNotificationHeadsUp();
- }
- }
+ mEntryManager.onHeadsUpStateChanged(entry, isHeadsUp);
- }
-
- protected void updateHeadsUp(String key, Entry entry, boolean shouldPeek,
- boolean alertAgain) {
- final boolean wasHeadsUp = isHeadsUp(key);
- if (wasHeadsUp) {
- if (!shouldPeek) {
- // We don't want this to be interrupting anymore, lets remove it
- mHeadsUpManager.removeNotification(key, false /* ignoreEarliestRemovalTime */);
- } else {
- mHeadsUpManager.updateNotification(entry, alertAgain);
- }
- } else if (shouldPeek && alertAgain) {
- // This notification was updated to be a heads-up, show it!
- mHeadsUpManager.showNotification(entry);
+ if (isHeadsUp) {
+ mDozeServiceHost.fireNotificationHeadsUp();
}
}
@@ -2626,14 +1952,6 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
- public boolean isHeadsUp(String key) {
- return mHeadsUpManager.isHeadsUp(key);
- }
-
- protected boolean isSnoozedPackage(StatusBarNotification sbn) {
- return mHeadsUpManager.isSnoozed(sbn.getPackageName());
- }
-
public boolean isKeyguardCurrentlySecure() {
return !mUnlockMethodCache.canSkipBouncer();
}
@@ -2651,17 +1969,8 @@ public class StatusBar extends SystemUI implements DemoMode,
}
if (!isExpanded) {
- removeRemoteInputEntriesKeptUntilCollapsed();
- }
- }
-
- private void removeRemoteInputEntriesKeptUntilCollapsed() {
- for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) {
- Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i);
- mRemoteInputController.removeRemoteInput(entry, null);
- removeNotification(entry.key, mLatestRankingMap);
+ mRemoteInputManager.removeRemoteInputEntriesKeptUntilCollapsed();
}
- mRemoteInputEntriesToRemoveOnCollapse.clear();
}
public NotificationStackScrollLayout getNotificationScrollLayout() {
@@ -2672,11 +1981,6 @@ public class StatusBar extends SystemUI implements DemoMode,
return mDozeScrimController != null && mDozeScrimController.isPulsing();
}
- @Override
- public void onReorderingAllowed() {
- updateNotifications();
- }
-
public boolean isLaunchTransitionFadingAway() {
return mLaunchTransitionFadingAway;
}
@@ -2694,7 +1998,7 @@ public class StatusBar extends SystemUI implements DemoMode,
OverlayInfo themeInfo = null;
try {
themeInfo = mOverlayManager.getOverlayInfo("com.android.systemui.theme.dark",
- mCurrentUserId);
+ mLockscreenUserManager.getCurrentUserId());
} catch (RemoteException e) {
e.printStackTrace();
}
@@ -3247,19 +2551,12 @@ public class StatusBar extends SystemUI implements DemoMode,
if ((mSystemUiVisibility & STATUS_OR_NAV_TRANSIENT) != 0 // a transient bar is revealed
&& event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
&& event.getX() == 0 && event.getY() == 0 // a touch outside both bars
- && !mRemoteInputController.isRemoteInputActive()) { // not due to typing in IME
+ && !mRemoteInputManager.getController()
+ .isRemoteInputActive()) { // not due to typing in IME
userAutohide();
}
}
- private void checkRemoteInputOutside(MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
- && event.getX() == 0 && event.getY() == 0 // a touch outside both bars
- && mRemoteInputController.isRemoteInputActive()) {
- mRemoteInputController.closeRemoteInputs();
- }
- }
-
private void userAutohide() {
cancelAutohide();
mHandler.postDelayed(mAutohide, 350); // longer than app gesture -> flag clear
@@ -3317,14 +2614,6 @@ public class StatusBar extends SystemUI implements DemoMode,
+ " scroll " + mStackScroller.getScrollX()
+ "," + mStackScroller.getScrollY());
}
- pw.print(" mPendingNotifications=");
- if (mPendingNotifications.size() == 0) {
- pw.println("null");
- } else {
- for (Entry entry : mPendingNotifications.values()) {
- pw.println(entry.notification);
- }
- }
pw.print(" mInteractingWindows="); pw.println(mInteractingWindows);
pw.print(" mStatusBarWindowState=");
@@ -3333,13 +2622,10 @@ public class StatusBar extends SystemUI implements DemoMode,
pw.println(BarTransitions.modeToString(mStatusBarMode));
pw.print(" mDozing="); pw.println(mDozing);
pw.print(" mZenMode=");
- pw.println(Settings.Global.zenModeToString(mZenMode));
- pw.print(" mUseHeadsUp=");
- pw.println(mUseHeadsUp);
- pw.print(" mGutsManager: ");
- if (mGutsManager != null) {
- mGutsManager.dump(fd, pw, args);
- }
+ pw.println(Settings.Global.zenModeToString(Settings.Global.getInt(
+ mContext.getContentResolver(), Settings.Global.ZEN_MODE,
+ Settings.Global.ZEN_MODE_OFF)));
+
if (mStatusBarView != null) {
dumpBarTransitions(pw, "mStatusBarView", mStatusBarView.getBarTransitions());
}
@@ -3381,8 +2667,8 @@ public class StatusBar extends SystemUI implements DemoMode,
}
if (DUMPTRUCK) {
- synchronized (mNotificationData) {
- mNotificationData.dump(pw, " ");
+ synchronized (mEntryManager.getNotificationData()) {
+ mEntryManager.getNotificationData().dump(pw, " ");
}
if (false) {
@@ -3442,19 +2728,20 @@ public class StatusBar extends SystemUI implements DemoMode,
private void addStatusBarWindow() {
makeStatusBarView();
mStatusBarWindowManager = Dependency.get(StatusBarWindowManager.class);
- mRemoteInputController = new RemoteInputController(new RemoteInputController.Delegate() {
- public void setRemoteInputActive(NotificationData.Entry entry,
- boolean remoteInputActive) {
- mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
- }
- public void lockScrollTo(NotificationData.Entry entry) {
- mStackScroller.lockScrollTo(entry.row);
- }
- public void requestDisallowLongPressAndDismiss() {
- mStackScroller.requestDisallowLongPress();
- mStackScroller.requestDisallowDismiss();
- }
- });
+ mRemoteInputManager.setUpWithPresenter(this, mEntryManager, this,
+ new RemoteInputController.Delegate() {
+ public void setRemoteInputActive(NotificationData.Entry entry,
+ boolean remoteInputActive) {
+ mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
+ }
+ public void lockScrollTo(NotificationData.Entry entry) {
+ mStackScroller.lockScrollTo(entry.row);
+ }
+ public void requestDisallowLongPressAndDismiss() {
+ mStackScroller.requestDisallowLongPress();
+ mStackScroller.requestDisallowDismiss();
+ }
+ });
mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight());
}
@@ -3484,7 +2771,7 @@ public class StatusBar extends SystemUI implements DemoMode,
if (onlyProvisioned && !isDeviceProvisioned()) return;
final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
- mContext, intent, mCurrentUserId);
+ mContext, intent, mLockscreenUserManager.getCurrentUserId());
Runnable runnable = () -> {
mAssistManager.hideAssist();
intent.setFlags(
@@ -3573,10 +2860,10 @@ public class StatusBar extends SystemUI implements DemoMode,
String action = intent.getAction();
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
KeyboardShortcuts.dismiss();
- if (mRemoteInputController != null) {
- mRemoteInputController.closeRemoteInputs();
+ if (mRemoteInputManager.getController() != null) {
+ mRemoteInputManager.getController().closeRemoteInputs();
}
- if (isCurrentProfile(getSendingUserId())) {
+ if (mLockscreenUserManager.isCurrentProfile(getSendingUserId())) {
int flags = CommandQueue.FLAG_EXCLUDE_NONE;
String reason = intent.getStringExtra("reason");
if (reason != null && reason.equals(SYSTEM_DIALOG_REASON_RECENT_APPS)) {
@@ -3621,7 +2908,8 @@ public class StatusBar extends SystemUI implements DemoMode,
};
public void resetUserExpandedStates() {
- ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+ ArrayList<Entry> activeNotifications = mEntryManager.getNotificationData()
+ .getActiveNotifications();
final int notificationCount = activeNotifications.size();
for (int i = 0; i < notificationCount; i++) {
NotificationData.Entry entry = activeNotifications.get(i);
@@ -3664,27 +2952,40 @@ public class StatusBar extends SystemUI implements DemoMode,
Log.v(TAG, "configuration changed: " + mContext.getResources().getConfiguration());
}
- updateRowStates();
+ mViewHierarchyManager.updateRowStates();
mScreenPinningRequest.onConfigurationChanged();
}
- public void userSwitched(int newUserId) {
+ @Override
+ public void onUserSwitched(int newUserId) {
// Begin old BaseStatusBar.userSwitched
setHeadsUpUser(newUserId);
// End old BaseStatusBar.userSwitched
if (MULTIUSER_DEBUG) mNotificationPanelDebugText.setText("USER " + newUserId);
animateCollapsePanels();
updatePublicMode();
- mNotificationData.filterAndSort();
+ mEntryManager.getNotificationData().filterAndSort();
if (mReinflateNotificationsOnUserSwitched) {
- updateNotificationsOnDensityOrFontScaleChanged();
+ mEntryManager.updateNotificationsOnDensityOrFontScaleChanged();
mReinflateNotificationsOnUserSwitched = false;
}
- updateNotificationShade();
+ updateNotificationViews();
mMediaManager.clearCurrentMediaNotification();
setLockscreenUser(newUserId);
}
+ @Override
+ public NotificationLockscreenUserManager getNotificationLockscreenUserManager() {
+ return mLockscreenUserManager;
+ }
+
+ @Override
+ public void onBindRow(Entry entry, PackageManager pmUser,
+ StatusBarNotification sbn, ExpandableNotificationRow row) {
+ row.setAboveShelfChangedListener(mAboveShelfObserver);
+ row.setSecureStateProvider(this::isKeyguardCurrentlySecure);
+ }
+
protected void setLockscreenUser(int newUserId) {
mLockscreenWallpaper.setCurrentUser(newUserId);
mScrimController.setCurrentUser(newUserId);
@@ -3734,9 +3035,9 @@ public class StatusBar extends SystemUI implements DemoMode,
protected void handleVisibleToUserChanged(boolean visibleToUser) {
if (visibleToUser) {
handleVisibleToUserChangedImpl(visibleToUser);
- startNotificationLogging();
+ mNotificationLogger.startNotificationLogging();
} else {
- stopNotificationLogging();
+ mNotificationLogger.stopNotificationLogging();
handleVisibleToUserChangedImpl(visibleToUser);
}
}
@@ -3745,7 +3046,8 @@ public class StatusBar extends SystemUI implements DemoMode,
try {
// consider the transition from peek to expanded to be a panel open,
// but not one that clears notification effects.
- int notificationLoad = mNotificationData.getActiveNotifications().size();
+ int notificationLoad = mEntryManager.getNotificationData()
+ .getActiveNotifications().size();
mBarService.onPanelRevealed(false, notificationLoad);
} catch (RemoteException ex) {
// Won't fail unless the world has ended.
@@ -3757,77 +3059,38 @@ public class StatusBar extends SystemUI implements DemoMode,
* See also StatusBar.setPanelExpanded for another place where we attempt to do this.
*/
private void handleVisibleToUserChangedImpl(boolean visibleToUser) {
- try {
- if (visibleToUser) {
- boolean pinnedHeadsUp = mHeadsUpManager.hasPinnedHeadsUp();
- boolean clearNotificationEffects =
- !isPresenterFullyCollapsed() &&
- (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED);
- int notificationLoad = mNotificationData.getActiveNotifications().size();
- if (pinnedHeadsUp && isPresenterFullyCollapsed()) {
- notificationLoad = 1;
+ if (visibleToUser) {
+ boolean pinnedHeadsUp = mHeadsUpManager.hasPinnedHeadsUp();
+ boolean clearNotificationEffects =
+ !isPresenterFullyCollapsed() &&
+ (mState == StatusBarState.SHADE
+ || mState == StatusBarState.SHADE_LOCKED);
+ int notificationLoad = mEntryManager.getNotificationData().getActiveNotifications()
+ .size();
+ if (pinnedHeadsUp && isPresenterFullyCollapsed()) {
+ notificationLoad = 1;
+ }
+ final int finalNotificationLoad = notificationLoad;
+ mUiOffloadThread.submit(() -> {
+ try {
+ mBarService.onPanelRevealed(clearNotificationEffects,
+ finalNotificationLoad);
+ } catch (RemoteException ex) {
+ // Won't fail unless the world has ended.
}
- mBarService.onPanelRevealed(clearNotificationEffects, notificationLoad);
- } else {
- mBarService.onPanelHidden();
- }
- } catch (RemoteException ex) {
- // Won't fail unless the world has ended.
- }
- }
-
- private void stopNotificationLogging() {
- // Report all notifications as invisible and turn down the
- // reporter.
- if (!mCurrentlyVisibleNotifications.isEmpty()) {
- logNotificationVisibilityChanges(
- Collections.emptyList(), mCurrentlyVisibleNotifications);
- recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
- }
- mHandler.removeCallbacks(mVisibilityReporter);
- mStackScroller.setChildLocationsChangedListener(null);
- }
-
- private void startNotificationLogging() {
- mStackScroller.setChildLocationsChangedListener(mNotificationLocationsChangedListener);
- // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't
- // cause the scroller to emit child location events. Hence generate
- // one ourselves to guarantee that we're reporting visible
- // notifications.
- // (Note that in cases where the scroller does emit events, this
- // additional event doesn't break anything.)
- mNotificationLocationsChangedListener.onChildLocationsChanged(mStackScroller);
- }
-
- private void logNotificationVisibilityChanges(
- Collection<NotificationVisibility> newlyVisible,
- Collection<NotificationVisibility> noLongerVisible) {
- if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
- return;
- }
- NotificationVisibility[] newlyVisibleAr =
- newlyVisible.toArray(new NotificationVisibility[newlyVisible.size()]);
- NotificationVisibility[] noLongerVisibleAr =
- noLongerVisible.toArray(new NotificationVisibility[noLongerVisible.size()]);
- try {
- mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr);
- } catch (RemoteException e) {
- // Ignore.
+ });
+ } else {
+ mUiOffloadThread.submit(() -> {
+ try {
+ mBarService.onPanelHidden();
+ } catch (RemoteException ex) {
+ // Won't fail unless the world has ended.
+ }
+ });
}
- final int N = newlyVisible.size();
- if (N > 0) {
- String[] newlyVisibleKeyAr = new String[N];
- for (int i = 0; i < N; i++) {
- newlyVisibleKeyAr[i] = newlyVisibleAr[i].key;
- }
-
- setNotificationsShown(newlyVisibleKeyAr);
- }
}
- // State logging
-
private void logStateToEventlog() {
boolean isShowing = mStatusBarKeyguardViewManager.isShowing();
boolean isOccluded = mStatusBarKeyguardViewManager.isOccluded();
@@ -3931,13 +3194,14 @@ public class StatusBar extends SystemUI implements DemoMode,
public void destroy() {
// Begin old BaseStatusBar.destroy().
- mContext.unregisterReceiver(mBaseBroadcastReceiver);
+ mContext.unregisterReceiver(mBannerActionBroadcastReceiver);
+ mLockscreenUserManager.destroy();
try {
mNotificationListener.unregisterAsSystemService();
} catch (RemoteException e) {
// Ignore.
}
- mDeviceProvisionedController.removeCallback(mDeviceProvisionedListener);
+ mEntryManager.destroy();
// End old BaseStatusBar.destroy().
if (mStatusBarWindow != null) {
mWindowManager.removeViewImmediate(mStatusBarWindow);
@@ -4185,14 +3449,10 @@ public class StatusBar extends SystemUI implements DemoMode,
.setStartDelay(0)
.setDuration(FADE_KEYGUARD_DURATION_PULSING)
.setInterpolator(ScrimController.KEYGUARD_FADE_OUT_INTERPOLATOR)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- hideKeyguard();
- mStatusBarKeyguardViewManager.onKeyguardFadedAway();
- }
- })
- .start();
+ .withEndAction(()-> {
+ hideKeyguard();
+ mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+ }).start();
}
/**
@@ -4337,18 +3597,21 @@ public class StatusBar extends SystemUI implements DemoMode,
mKeyguardMonitor.notifyKeyguardDoneFading();
}
+ // TODO: Move this to NotificationLockscreenUserManager.
private void updatePublicMode() {
final boolean showingKeyguard = mStatusBarKeyguardViewManager.isShowing();
final boolean devicePublic = showingKeyguard
- && mStatusBarKeyguardViewManager.isSecure(mCurrentUserId);
+ && mStatusBarKeyguardViewManager.isSecure(
+ mLockscreenUserManager.getCurrentUserId());
// Look for public mode users. Users are considered public in either case of:
// - device keyguard is shown in secure mode;
// - profile is locked with a work challenge.
- for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) {
- final int userId = mCurrentProfiles.valueAt(i).id;
+ SparseArray<UserInfo> currentProfiles = mLockscreenUserManager.getCurrentProfiles();
+ for (int i = currentProfiles.size() - 1; i >= 0; i--) {
+ final int userId = currentProfiles.valueAt(i).id;
boolean isProfilePublic = devicePublic;
- if (!devicePublic && userId != mCurrentUserId) {
+ if (!devicePublic && userId != mLockscreenUserManager.getCurrentUserId()) {
// We can't rely on KeyguardManager#isDeviceLocked() for unified profile challenge
// due to a race condition where this code could be called before
// TrustManagerService updates its internal records, resulting in an incorrect
@@ -4358,7 +3621,7 @@ public class StatusBar extends SystemUI implements DemoMode,
isProfilePublic = mKeyguardManager.isDeviceLocked(userId);
}
}
- setLockscreenPublicMode(isProfilePublic, userId);
+ mLockscreenUserManager.setLockscreenPublicMode(isProfilePublic, userId);
}
}
@@ -4391,7 +3654,7 @@ public class StatusBar extends SystemUI implements DemoMode,
updateDozingState();
updatePublicMode();
updateStackScrollerState(goingToFullShade, fromShadeLocked);
- updateNotifications();
+ mEntryManager.updateNotifications();
checkBarModes();
updateScrimController();
updateMediaMetaData(false, mState != StatusBarState.KEYGUARD);
@@ -4416,7 +3679,7 @@ public class StatusBar extends SystemUI implements DemoMode,
mUiOffloadThread.submit(() -> {
try {
mOverlayManager.setEnabled("com.android.systemui.theme.dark",
- useDarkTheme, mCurrentUserId);
+ useDarkTheme, mLockscreenUserManager.getCurrentUserId());
} catch (RemoteException e) {
Log.w(TAG, "Can't change theme", e);
}
@@ -4454,21 +3717,22 @@ public class StatusBar extends SystemUI implements DemoMode,
private void updateDozingState() {
Trace.traceCounter(Trace.TRACE_TAG_APP, "dozing", mDozing ? 1 : 0);
Trace.beginSection("StatusBar#updateDozingState");
- boolean animate = !mDozing && mDozeServiceHost.shouldAnimateWakeup();
+ boolean animate = (!mDozing && mDozeServiceHost.shouldAnimateWakeup())
+ || (mDozing && mDozeServiceHost.shouldAnimateScreenOff());
mNotificationPanel.setDozing(mDozing, animate);
mStackScroller.setDark(mDozing, animate, mWakeUpTouchLocation);
mDozeScrimController.setDozing(mDozing);
mKeyguardIndicationController.setDozing(mDozing);
mNotificationPanel.setDark(mDozing, animate);
updateQsExpansionEnabled();
- updateRowStates();
+ mViewHierarchyManager.updateRowStates();
Trace.endSection();
}
public void updateStackScrollerState(boolean goingToFullShade, boolean fromShadeLocked) {
if (mStackScroller == null) return;
boolean onKeyguard = mState == StatusBarState.KEYGUARD;
- boolean publicMode = isAnyProfilePublicMode();
+ boolean publicMode = mLockscreenUserManager.isAnyProfilePublicMode();
mStackScroller.setHideSensitive(publicMode, goingToFullShade);
mStackScroller.setDimmed(onKeyguard, fromShadeLocked /* animate */);
mStackScroller.setExpandingEnabled(!onKeyguard);
@@ -4591,7 +3855,7 @@ public class StatusBar extends SystemUI implements DemoMode,
clearNotificationEffects();
}
if (state == StatusBarState.KEYGUARD) {
- removeRemoteInputEntriesKeptUntilCollapsed();
+ mRemoteInputManager.removeRemoteInputEntriesKeptUntilCollapsed();
maybeEscalateHeadsUp();
}
mState = state;
@@ -4665,7 +3929,8 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
- protected int getMaxKeyguardNotifications(boolean recompute) {
+ @Override
+ public int getMaxNotificationsWhileLocked(boolean recompute) {
if (recompute) {
mMaxKeyguardNotifications = Math.max(1,
mNotificationPanel.computeMaxKeyguardNotifications(
@@ -4675,8 +3940,8 @@ public class StatusBar extends SystemUI implements DemoMode,
return mMaxKeyguardNotifications;
}
- public int getMaxKeyguardNotifications() {
- return getMaxKeyguardNotifications(false /* recompute */);
+ public int getMaxNotificationsWhileLocked() {
+ return getMaxNotificationsWhileLocked(false /* recompute */);
}
// TODO: Figure out way to remove these.
@@ -4762,7 +4027,7 @@ public class StatusBar extends SystemUI implements DemoMode,
return;
}
- int userId = mCurrentUserId;
+ int userId = mLockscreenUserManager.getCurrentUserId();
ExpandableNotificationRow row = null;
if (expandView instanceof ExpandableNotificationRow) {
row = (ExpandableNotificationRow) expandView;
@@ -4774,9 +4039,11 @@ public class StatusBar extends SystemUI implements DemoMode,
userId = row.getStatusBarNotification().getUserId();
}
}
- boolean fullShadeNeedsBouncer = !userAllowsPrivateNotificationsInPublic(mCurrentUserId)
- || !mShowLockscreenNotifications || mFalsingManager.shouldEnforceBouncer();
- if (isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) {
+ boolean fullShadeNeedsBouncer = !mLockscreenUserManager.
+ userAllowsPrivateNotificationsInPublic(mLockscreenUserManager.getCurrentUserId())
+ || !mLockscreenUserManager.shouldShowLockscreenNotifications()
+ || mFalsingManager.shouldEnforceBouncer();
+ if (mLockscreenUserManager.isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) {
mLeaveOpenOnKeyguardHide = true;
showBouncerIfKeyguard();
mDraggedDownRow = row;
@@ -4793,13 +4060,15 @@ public class StatusBar extends SystemUI implements DemoMode,
dismissKeyguardThenExecute(dismissAction, true /* afterKeyguardGone */);
}
- protected void onLockedRemoteInput(ExpandableNotificationRow row, View clicked) {
+ @Override
+ public void onLockedRemoteInput(ExpandableNotificationRow row, View clicked) {
mLeaveOpenOnKeyguardHide = true;
showBouncer();
mPendingRemoteInputView = clicked;
}
- protected void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row,
+ @Override
+ public void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row,
View clickedView) {
if (isKeyguardShowing()) {
onLockedRemoteInput(row, clickedView);
@@ -4809,6 +4078,47 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
+ @Override
+ public boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent) {
+ // Skip remote input as doing so will expand the notification shade.
+ return (mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0;
+ }
+
+ @Override
+ public boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
+ Intent fillInIntent, NotificationRemoteInputManager.ClickHandler defaultHandler) {
+ final boolean isActivity = pendingIntent.isActivity();
+ if (isActivity) {
+ final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
+ mContext, pendingIntent.getIntent(), mLockscreenUserManager.getCurrentUserId());
+ dismissKeyguardThenExecute(() -> {
+ try {
+ ActivityManager.getService().resumeAppSwitches();
+ } catch (RemoteException e) {
+ }
+
+ boolean handled = defaultHandler.handleClick();
+
+ // close the shade if it was open
+ if (handled && !mNotificationPanel.isFullyCollapsed()) {
+ animateCollapsePanels(
+ CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */);
+ visibilityChanged(false);
+ mAssistManager.hideAssist();
+
+ // Wait for activity start.
+ return true;
+ } else {
+ return false;
+ }
+
+ }, afterKeyguardGone);
+ return true;
+ } else {
+ return defaultHandler.handleClick();
+ }
+ }
+
protected boolean startWorkChallengeIfNecessary(int userId, IntentSender intendSender,
String notificationKey) {
// Clear pending remote view, as we do not want to trigger pending remote input view when
@@ -4845,7 +4155,8 @@ public class StatusBar extends SystemUI implements DemoMode,
// End old BaseStatusBar.startWorkChallengeIfNecessary.
}
- protected void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row,
+ @Override
+ public void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row,
View clicked) {
// Collapse notification and show work challenge
animateCollapsePanels();
@@ -4855,19 +4166,12 @@ public class StatusBar extends SystemUI implements DemoMode,
mPendingWorkRemoteInputView = clicked;
}
- private boolean isAnyProfilePublicMode() {
- for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) {
- if (isLockscreenPublicMode(mCurrentProfiles.valueAt(i).id)) {
- return true;
- }
- }
- return false;
- }
-
- protected void onWorkChallengeChanged() {
+ @Override
+ public void onWorkChallengeChanged() {
updatePublicMode();
- updateNotifications();
- if (mPendingWorkRemoteInputView != null && !isAnyProfilePublicMode()) {
+ mEntryManager.updateNotifications();
+ if (mPendingWorkRemoteInputView != null
+ && !mLockscreenUserManager.isAnyProfilePublicMode()) {
// Expand notification panel and the notification row, then click on remote input view
final Runnable clickPendingViewRunnable = () -> {
final View pendingWorkRemoteInputView = mPendingWorkRemoteInputView;
@@ -5075,9 +4379,10 @@ public class StatusBar extends SystemUI implements DemoMode,
}
public boolean hasActiveNotifications() {
- return !mNotificationData.getActiveNotifications().isEmpty();
+ return !mEntryManager.getNotificationData().getActiveNotifications().isEmpty();
}
+ @Override
public void wakeUpIfDozing(long time, View where) {
if (mDozing) {
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
@@ -5092,6 +4397,11 @@ public class StatusBar extends SystemUI implements DemoMode,
}
@Override
+ public boolean isDeviceLocked(int userId) {
+ return mKeyguardManager.isDeviceLocked(userId);
+ }
+
+ @Override
public void appTransitionCancelled() {
EventBus.getDefault().send(new AppTransitionFinishedEvent());
}
@@ -5147,12 +4457,14 @@ public class StatusBar extends SystemUI implements DemoMode,
}
boolean isCameraAllowedByAdmin() {
- if (mDevicePolicyManager.getCameraDisabled(null, mCurrentUserId)) {
+ if (mDevicePolicyManager.getCameraDisabled(null,
+ mLockscreenUserManager.getCurrentUserId())) {
return false;
} else if (mStatusBarKeyguardViewManager == null ||
(isKeyguardShowing() && isKeyguardSecure())) {
// Check if the admin has disabled the camera specifically for the keyguard
- return (mDevicePolicyManager.getKeyguardDisabledFeatures(null, mCurrentUserId)
+ return (mDevicePolicyManager.
+ getKeyguardDisabledFeatures(null, mLockscreenUserManager.getCurrentUserId())
& DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) == 0;
}
@@ -5171,6 +4483,7 @@ public class StatusBar extends SystemUI implements DemoMode,
public void notifyFpAuthModeChanged() {
updateDozing();
+ updateScrimController();
}
private void updateDozing() {
@@ -5195,7 +4508,15 @@ public class StatusBar extends SystemUI implements DemoMode,
Trace.endSection();
}
- public void updateScrimController() {
+ @VisibleForTesting
+ void updateScrimController() {
+ Trace.beginSection("StatusBar#updateScrimController");
+
+ // We don't want to end up in KEYGUARD state when we're unlocking with
+ // fingerprint from doze. We should cross fade directly from black.
+ final boolean wakeAndUnlocking = mFingerprintUnlockController.getMode()
+ == FingerprintUnlockController.MODE_WAKE_AND_UNLOCK;
+
if (mBouncerShowing) {
mScrimController.transitionTo(ScrimState.BOUNCER);
} else if (mLaunchCameraOnScreenTurningOn || isInLaunchTransition()) {
@@ -5206,11 +4527,12 @@ public class StatusBar extends SystemUI implements DemoMode,
// Handled in DozeScrimController#setPulsing
} else if (mDozing) {
mScrimController.transitionTo(ScrimState.AOD);
- } else if (mIsKeyguard) {
+ } else if (mIsKeyguard && !wakeAndUnlocking) {
mScrimController.transitionTo(ScrimState.KEYGUARD);
} else {
mScrimController.transitionTo(ScrimState.UNLOCKED, mUnlockScrimCallback);
}
+ Trace.endSection();
}
public boolean isKeyguardShowing() {
@@ -5224,6 +4546,7 @@ public class StatusBar extends SystemUI implements DemoMode,
private final class DozeServiceHost implements DozeHost {
private final ArrayList<Callback> mCallbacks = new ArrayList<>();
private boolean mAnimateWakeup;
+ private boolean mAnimateScreenOff;
private boolean mIgnoreTouchWhilePulsing;
@Override
@@ -5371,6 +4694,11 @@ public class StatusBar extends SystemUI implements DemoMode,
}
@Override
+ public void setAnimateScreenOff(boolean animateScreenOff) {
+ mAnimateScreenOff = animateScreenOff;
+ }
+
+ @Override
public void onDoubleTap(float screenX, float screenY) {
if (screenX > 0 && screenY > 0 && mAmbientIndicationContainer != null
&& mAmbientIndicationContainer.getVisibility() == View.VISIBLE) {
@@ -5414,6 +4742,10 @@ public class StatusBar extends SystemUI implements DemoMode,
private boolean shouldAnimateWakeup() {
return mAnimateWakeup;
}
+
+ public boolean shouldAnimateScreenOff() {
+ return mAnimateScreenOff;
+ }
}
public boolean shouldIgnoreTouch() {
@@ -5426,12 +4758,10 @@ public class StatusBar extends SystemUI implements DemoMode,
protected IStatusBarService mBarService;
// all notifications
- protected NotificationData mNotificationData;
protected NotificationStackScrollLayout mStackScroller;
- protected final NotificationGroupManager mGroupManager = new NotificationGroupManager();
+ protected NotificationGroupManager mGroupManager;
- protected RemoteInputController mRemoteInputController;
// for heads up notifications
protected HeadsUpManager mHeadsUpManager;
@@ -5439,48 +4769,25 @@ public class StatusBar extends SystemUI implements DemoMode,
private AboveShelfObserver mAboveShelfObserver;
// handling reordering
- protected final VisualStabilityManager mVisualStabilityManager = new VisualStabilityManager();
-
- protected int mCurrentUserId = 0;
- final protected SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>();
+ protected VisualStabilityManager mVisualStabilityManager;
protected AccessibilityManager mAccessibilityManager;
protected boolean mDeviceInteractive;
protected boolean mVisible;
- protected final ArraySet<Entry> mHeadsUpEntriesToRemoveOnSwitch = new ArraySet<>();
- protected final ArraySet<Entry> mRemoteInputEntriesToRemoveOnCollapse = new ArraySet<>();
-
- /**
- * Notifications with keys in this set are not actually around anymore. We kept them around
- * when they were canceled in response to a remote input interaction. This allows us to show
- * what you replied and allows you to continue typing into it.
- */
- protected final ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>();
// mScreenOnFromKeyguard && mVisible.
private boolean mVisibleToUser;
- protected boolean mUseHeadsUp = false;
- protected boolean mDisableNotificationAlerts = false;
-
protected DevicePolicyManager mDevicePolicyManager;
protected PowerManager mPowerManager;
protected StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
- // public mode, private notifications, etc
- private final SparseBooleanArray mLockscreenPublicMode = new SparseBooleanArray();
- private final SparseBooleanArray mUsersAllowingPrivateNotifications = new SparseBooleanArray();
- private final SparseBooleanArray mUsersAllowingNotifications = new SparseBooleanArray();
-
- private UserManager mUserManager;
-
protected KeyguardManager mKeyguardManager;
private LockPatternUtils mLockPatternUtils;
private DeviceProvisionedController mDeviceProvisionedController
= Dependency.get(DeviceProvisionedController.class);
- protected SystemServicesProxy mSystemServicesProxy;
// UI-specific methods
@@ -5491,14 +4798,10 @@ public class StatusBar extends SystemUI implements DemoMode,
protected RecentsComponent mRecents;
- protected int mZenMode;
-
protected NotificationShelf mNotificationShelf;
protected DismissView mDismissView;
protected EmptyShadeView mEmptyShadeView;
- private final NotificationClicker mNotificationClicker = new NotificationClicker();
-
protected AssistManager mAssistManager;
protected boolean mVrMode;
@@ -5523,288 +4826,15 @@ public class StatusBar extends SystemUI implements DemoMode,
return mVrMode;
}
- private final DeviceProvisionedListener mDeviceProvisionedListener =
- new DeviceProvisionedListener() {
- @Override
- public void onDeviceProvisionedChanged() {
- updateNotifications();
- }
- };
-
- protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- final int mode = Settings.Global.getInt(mContext.getContentResolver(),
- Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_OFF);
- setZenMode(mode);
-
- updateLockscreenNotificationSetting();
- }
- };
-
- private final ContentObserver mLockscreenSettingsObserver = new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- // We don't know which user changed LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS or
- // LOCK_SCREEN_SHOW_NOTIFICATIONS, so we just dump our cache ...
- mUsersAllowingPrivateNotifications.clear();
- mUsersAllowingNotifications.clear();
- // ... and refresh all the notifications
- updateLockscreenNotificationSetting();
- updateNotifications();
- }
- };
-
- private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
-
- @Override
- public boolean onClickHandler(
- final View view, final PendingIntent pendingIntent, final Intent fillInIntent) {
- wakeUpIfDozing(SystemClock.uptimeMillis(), view);
-
- if (handleRemoteInput(view, pendingIntent)) {
- return true;
- }
-
- if (DEBUG) {
- Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
- }
- logActionClick(view);
- // The intent we are sending is for the application, which
- // won't have permission to immediately start an activity after
- // the user switches to home. We know it is safe to do at this
- // point, so make sure new activity switches are now allowed.
- try {
- ActivityManager.getService().resumeAppSwitches();
- } catch (RemoteException e) {
- }
- final boolean isActivity = pendingIntent.isActivity();
- if (isActivity) {
- final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
- mContext, pendingIntent.getIntent(), mCurrentUserId);
- dismissKeyguardThenExecute(() -> {
- try {
- ActivityManager.getService().resumeAppSwitches();
- } catch (RemoteException e) {
- }
-
- boolean handled = superOnClickHandler(view, pendingIntent, fillInIntent);
-
- // close the shade if it was open
- if (handled && !mNotificationPanel.isFullyCollapsed()) {
- animateCollapsePanels(
- CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */);
- visibilityChanged(false);
- mAssistManager.hideAssist();
-
- // Wait for activity start.
- return true;
- } else {
- return false;
- }
-
- }, afterKeyguardGone);
- return true;
- } else {
- return superOnClickHandler(view, pendingIntent, fillInIntent);
- }
- }
-
- private void logActionClick(View view) {
- ViewParent parent = view.getParent();
- String key = getNotificationKeyForParent(parent);
- if (key == null) {
- Log.w(TAG, "Couldn't determine notification for click.");
- return;
- }
- int index = -1;
- // If this is a default template, determine the index of the button.
- if (view.getId() == com.android.internal.R.id.action0 &&
- parent != null && parent instanceof ViewGroup) {
- ViewGroup actionGroup = (ViewGroup) parent;
- index = actionGroup.indexOfChild(view);
- }
- try {
- mBarService.onNotificationActionClick(key, index);
- } catch (RemoteException e) {
- // Ignore
- }
- }
-
- private String getNotificationKeyForParent(ViewParent parent) {
- while (parent != null) {
- if (parent instanceof ExpandableNotificationRow) {
- return ((ExpandableNotificationRow) parent).getStatusBarNotification().getKey();
- }
- parent = parent.getParent();
- }
- return null;
- }
-
- private boolean superOnClickHandler(View view, PendingIntent pendingIntent,
- Intent fillInIntent) {
- return super.onClickHandler(view, pendingIntent, fillInIntent,
- WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
- }
-
- private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
- if ((mDisabled2 & StatusBarManager.DISABLE2_NOTIFICATION_SHADE) != 0) {
- // Skip remote input as doing so will expand the notification shade.
- return true;
- }
-
- Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
- RemoteInput[] inputs = null;
- if (tag instanceof RemoteInput[]) {
- inputs = (RemoteInput[]) tag;
- }
-
- if (inputs == null) {
- return false;
- }
-
- RemoteInput input = null;
-
- for (RemoteInput i : inputs) {
- if (i.getAllowFreeFormInput()) {
- input = i;
- }
- }
-
- if (input == null) {
- return false;
- }
-
- ViewParent p = view.getParent();
- RemoteInputView riv = null;
- while (p != null) {
- if (p instanceof View) {
- View pv = (View) p;
- if (pv.isRootNamespace()) {
- riv = findRemoteInputView(pv);
- break;
- }
- }
- p = p.getParent();
- }
- ExpandableNotificationRow row = null;
- while (p != null) {
- if (p instanceof ExpandableNotificationRow) {
- row = (ExpandableNotificationRow) p;
- break;
- }
- p = p.getParent();
- }
-
- if (row == null) {
- return false;
- }
-
- row.setUserExpanded(true);
-
- if (!mAllowLockscreenRemoteInput) {
- final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
- if (isLockscreenPublicMode(userId)) {
- onLockedRemoteInput(row, view);
- return true;
- }
- if (mUserManager.getUserInfo(userId).isManagedProfile()
- && mKeyguardManager.isDeviceLocked(userId)) {
- onLockedWorkRemoteInput(userId, row, view);
- return true;
- }
- }
-
- if (riv == null) {
- riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
- if (riv == null) {
- return false;
- }
- if (!row.getPrivateLayout().getExpandedChild().isShown()) {
- onMakeExpandedVisibleForRemoteInput(row, view);
- return true;
- }
- }
-
- int width = view.getWidth();
- if (view instanceof TextView) {
- // Center the reveal on the text which might be off-center from the TextView
- TextView tv = (TextView) view;
- if (tv.getLayout() != null) {
- int innerWidth = (int) tv.getLayout().getLineWidth(0);
- innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
- width = Math.min(width, innerWidth);
- }
- }
- int cx = view.getLeft() + width / 2;
- int cy = view.getTop() + view.getHeight() / 2;
- int w = riv.getWidth();
- int h = riv.getHeight();
- int r = Math.max(
- Math.max(cx + cy, cx + (h - cy)),
- Math.max((w - cx) + cy, (w - cx) + (h - cy)));
-
- riv.setRevealParameters(cx, cy, r);
- riv.setPendingIntent(pendingIntent);
- riv.setRemoteInput(inputs, input);
- riv.focusAnimated();
-
- return true;
- }
-
- private RemoteInputView findRemoteInputView(View v) {
- if (v == null) {
- return null;
- }
- return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
- }
- };
-
- private final BroadcastReceiver mBaseBroadcastReceiver = new BroadcastReceiver() {
+ private final BroadcastReceiver mBannerActionBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- if (Intent.ACTION_USER_SWITCHED.equals(action)) {
- mCurrentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
- updateCurrentProfilesCache();
- Log.v(TAG, "userId " + mCurrentUserId + " is in the house");
-
- updateLockscreenNotificationSetting();
-
- userSwitched(mCurrentUserId);
- } else if (Intent.ACTION_USER_ADDED.equals(action)) {
- updateCurrentProfilesCache();
- } else if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
- // Start the overview connection to the launcher service
- Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser();
- } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
- List<ActivityManager.RecentTaskInfo> recentTask = null;
- try {
- recentTask = ActivityManager.getService().getRecentTasks(1,
- ActivityManager.RECENT_WITH_EXCLUDED,
- mCurrentUserId).getList();
- } catch (RemoteException e) {
- // Abandon hope activity manager not running.
- }
- if (recentTask != null && recentTask.size() > 0) {
- UserInfo user = mUserManager.getUserInfo(recentTask.get(0).userId);
- if (user != null && user.isManagedProfile()) {
- Toast toast = Toast.makeText(mContext,
- R.string.managed_profile_foreground_toast,
- Toast.LENGTH_SHORT);
- TextView text = toast.getView().findViewById(android.R.id.message);
- text.setCompoundDrawablesRelativeWithIntrinsicBounds(
- R.drawable.stat_sys_managed_profile_status, 0, 0, 0);
- int paddingPx = mContext.getResources().getDimensionPixelSize(
- R.dimen.managed_profile_toast_padding);
- text.setCompoundDrawablePadding(paddingPx);
- toast.show();
- }
- }
- } else if (BANNER_ACTION_CANCEL.equals(action) || BANNER_ACTION_SETUP.equals(action)) {
+ if (BANNER_ACTION_CANCEL.equals(action) || BANNER_ACTION_SETUP.equals(action)) {
NotificationManager noMan = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- noMan.cancel(SystemMessage.NOTE_HIDDEN_NOTIFICATIONS);
+ noMan.cancel(com.android.internal.messages.nano.SystemMessageProto.SystemMessage.
+ NOTE_HIDDEN_NOTIFICATIONS);
Settings.Secure.putInt(mContext.getContentResolver(),
Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0);
@@ -5816,142 +4846,127 @@ public class StatusBar extends SystemUI implements DemoMode,
);
}
- } else if (NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION.equals(action)) {
- final IntentSender intentSender = intent.getParcelableExtra(Intent.EXTRA_INTENT);
- final String notificationKey = intent.getStringExtra(Intent.EXTRA_INDEX);
- if (intentSender != null) {
- try {
- mContext.startIntentSender(intentSender, null, 0, 0, 0);
- } catch (IntentSender.SendIntentException e) {
- /* ignore */
- }
- }
- if (notificationKey != null) {
- try {
- mBarService.onNotificationClick(notificationKey);
- } catch (RemoteException e) {
- /* ignore */
- }
- }
}
}
};
- private final BroadcastReceiver mAllUsersReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
-
- if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(action) &&
- isCurrentProfile(getSendingUserId())) {
- mUsersAllowingPrivateNotifications.clear();
- updateLockscreenNotificationSetting();
- updateNotifications();
- } else if (Intent.ACTION_DEVICE_LOCKED_CHANGED.equals(action)) {
- if (userId != mCurrentUserId && isCurrentProfile(userId)) {
- onWorkChallengeChanged();
- }
- }
- }
- };
+ @Override
+ public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) {
+ Notification notification = sbn.getNotification();
+ final PendingIntent intent = notification.contentIntent != null
+ ? notification.contentIntent
+ : notification.fullScreenIntent;
+ final String notificationKey = sbn.getKey();
- private final NotificationListenerWithPlugins mNotificationListener =
- new NotificationListenerWithPlugins() {
- @Override
- public void onListenerConnected() {
- if (DEBUG) Log.d(TAG, "onListenerConnected");
- onPluginConnected();
- final StatusBarNotification[] notifications = getActiveNotifications();
- if (notifications == null) {
- Log.w(TAG, "onListenerConnected unable to get active notifications.");
- return;
+ final boolean afterKeyguardGone = intent.isActivity()
+ && PreviewInflater.wouldLaunchResolverActivity(mContext, intent.getIntent(),
+ mLockscreenUserManager.getCurrentUserId());
+ dismissKeyguardThenExecute(() -> {
+ // TODO: Some of this code may be able to move to NotificationEntryManager.
+ if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUp(notificationKey)) {
+ // Release the HUN notification to the shade.
+
+ if (isPresenterFullyCollapsed()) {
+ HeadsUpManager.setIsClickedNotification(row, true);
+ }
+ //
+ // In most cases, when FLAG_AUTO_CANCEL is set, the notification will
+ // become canceled shortly by NoMan, but we can't assume that.
+ mHeadsUpManager.releaseImmediately(notificationKey);
+ }
+ StatusBarNotification parentToCancel = null;
+ if (shouldAutoCancel(sbn) && mGroupManager.isOnlyChildInGroup(sbn)) {
+ StatusBarNotification summarySbn =
+ mGroupManager.getLogicalGroupSummary(sbn).getStatusBarNotification();
+ if (shouldAutoCancel(summarySbn)) {
+ parentToCancel = summarySbn;
+ }
}
- final RankingMap currentRanking = getCurrentRanking();
- mHandler.post(() -> {
- for (StatusBarNotification sbn : notifications) {
- try {
- addNotification(sbn, currentRanking);
- } catch (InflationException e) {
- handleInflationException(sbn, e);
- }
+ final StatusBarNotification parentToCancelFinal = parentToCancel;
+ final Runnable runnable = () -> {
+ try {
+ // The intent we are sending is for the application, which
+ // won't have permission to immediately start an activity after
+ // the user switches to home. We know it is safe to do at this
+ // point, so make sure new activity switches are now allowed.
+ ActivityManager.getService().resumeAppSwitches();
+ } catch (RemoteException e) {
}
- });
- }
-
- @Override
- public void onNotificationPosted(final StatusBarNotification sbn,
- final RankingMap rankingMap) {
- if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn);
- if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
- mHandler.post(() -> {
- processForRemoteInput(sbn.getNotification());
- String key = sbn.getKey();
- mKeysKeptForRemoteInput.remove(key);
- boolean isUpdate = mNotificationData.get(key) != null;
- // In case we don't allow child notifications, we ignore children of
- // notifications that have a summary, since we're not going to show them
- // anyway. This is true also when the summary is canceled,
- // because children are automatically canceled by NoMan in that case.
- if (!ENABLE_CHILD_NOTIFICATIONS
- && mGroupManager.isChildInGroupWithSummary(sbn)) {
- if (DEBUG) {
- Log.d(TAG, "Ignoring group child due to existing summary: " + sbn);
- }
-
- // Remove existing notification to avoid stale data.
- if (isUpdate) {
- removeNotification(key, rankingMap);
- } else {
- mNotificationData.updateRanking(rankingMap);
+ if (intent != null) {
+ // If we are launching a work activity and require to launch
+ // separate work challenge, we defer the activity action and cancel
+ // notification until work challenge is unlocked.
+ if (intent.isActivity()) {
+ final int userId = intent.getCreatorUserHandle().getIdentifier();
+ if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)
+ && mKeyguardManager.isDeviceLocked(userId)) {
+ // TODO(b/28935539): should allow certain activities to
+ // bypass work challenge
+ if (startWorkChallengeIfNecessary(userId, intent.getIntentSender(),
+ notificationKey)) {
+ // Show work challenge, do not run PendingIntent and
+ // remove notification
+ return;
+ }
}
- return;
}
try {
- if (isUpdate) {
- updateNotification(sbn, rankingMap);
- } else {
- addNotification(sbn, rankingMap);
- }
- } catch (InflationException e) {
- handleInflationException(sbn, e);
+ intent.send(null, 0, null, null, null, null, getActivityOptions());
+ } catch (PendingIntent.CanceledException e) {
+ // the stack trace isn't very helpful here.
+ // Just log the exception message.
+ Log.w(TAG, "Sending contentIntent failed: " + e);
+
+ // TODO: Dismiss Keyguard.
}
- });
- }
- }
+ if (intent.isActivity()) {
+ mAssistManager.hideAssist();
+ }
+ }
- @Override
- public void onNotificationRemoved(StatusBarNotification sbn,
- final RankingMap rankingMap) {
- if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn);
- if (sbn != null && !onPluginNotificationRemoved(sbn, rankingMap)) {
- final String key = sbn.getKey();
- mHandler.post(() -> removeNotification(key, rankingMap));
- }
- }
+ try {
+ mBarService.onNotificationClick(notificationKey);
+ } catch (RemoteException ex) {
+ // system process is dead if we're here.
+ }
+ if (parentToCancelFinal != null) {
+ // We have to post it to the UI thread for synchronization
+ mHandler.post(() -> {
+ Runnable removeRunnable =
+ () -> mEntryManager.performRemoveNotification(parentToCancelFinal);
+ if (isCollapsing()) {
+ // To avoid lags we're only performing the remove
+ // after the shade was collapsed
+ addPostCollapseAction(removeRunnable);
+ } else {
+ removeRunnable.run();
+ }
+ });
+ }
+ };
- @Override
- public void onNotificationRankingUpdate(final RankingMap rankingMap) {
- if (DEBUG) Log.d(TAG, "onRankingUpdate");
- if (rankingMap != null) {
- RankingMap r = onPluginRankingUpdate(rankingMap);
- mHandler.post(() -> updateNotificationRanking(r));
+ if (mStatusBarKeyguardViewManager.isShowing()
+ && mStatusBarKeyguardViewManager.isOccluded()) {
+ mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
+ } else {
+ new Thread(runnable).start();
}
- }
- };
+ if (!mNotificationPanel.isFullyCollapsed()) {
+ // close the shade if it was open
+ animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */,
+ true /* delayed */);
+ visibilityChanged(false);
- private void updateCurrentProfilesCache() {
- synchronized (mCurrentProfiles) {
- mCurrentProfiles.clear();
- if (mUserManager != null) {
- for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) {
- mCurrentProfiles.put(user.id, user);
- }
+ return true;
+ } else {
+ return false;
}
- }
+ }, afterKeyguardGone);
}
+ protected NotificationListener mNotificationListener;
+
protected void notifyUserAboutHiddenNotifications() {
if (0 != Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 1)) {
@@ -6006,27 +5021,9 @@ public class StatusBar extends SystemUI implements DemoMode,
final int notificationUserId = n.getUserId();
if (DEBUG && MULTIUSER_DEBUG) {
Log.v(TAG, String.format("%s: current userid: %d, notification userid: %d", n,
- mCurrentUserId, notificationUserId));
- }
- return isCurrentProfile(notificationUserId);
- }
-
- protected void setNotificationShown(StatusBarNotification n) {
- setNotificationsShown(new String[]{n.getKey()});
- }
-
- protected void setNotificationsShown(String[] keys) {
- try {
- mNotificationListener.setNotificationsShown(keys);
- } catch (RuntimeException e) {
- Log.d(TAG, "failed setNotificationsShown: ", e);
- }
- }
-
- protected boolean isCurrentProfile(int userId) {
- synchronized (mCurrentProfiles) {
- return userId == UserHandle.USER_ALL || mCurrentProfiles.get(userId) != null;
+ mLockscreenUserManager.getCurrentUserId(), notificationUserId));
}
+ return mLockscreenUserManager.isCurrentProfile(notificationUserId);
}
@Override
@@ -6058,15 +5055,15 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
- protected ExpandableNotificationRow.LongPressListener getNotificationLongClicker() {
- return (v, x, y, item) -> mGutsManager.openGuts(v, x, y, item);
- }
-
@Override
public void toggleSplitScreen() {
toggleSplitScreenMode(-1 /* metricsDockAction */, -1 /* metricsUndockAction */);
}
+ void awakenDreams() {
+ SystemServicesProxy.getInstance(mContext).awakenDreamsAsync();
+ }
+
@Override
public void preloadRecentApps() {
int msg = MSG_PRELOAD_RECENT_APPS;
@@ -6115,89 +5112,14 @@ public class StatusBar extends SystemUI implements DemoMode,
KeyboardShortcuts.dismiss();
}
- /**
- * Save the current "public" (locked and secure) state of the lockscreen.
- */
- public void setLockscreenPublicMode(boolean publicMode, int userId) {
- mLockscreenPublicMode.put(userId, publicMode);
- }
-
- public boolean isLockscreenPublicMode(int userId) {
- if (userId == UserHandle.USER_ALL) {
- return mLockscreenPublicMode.get(mCurrentUserId, false);
- }
- return mLockscreenPublicMode.get(userId, false);
- }
-
- /**
- * Has the given user chosen to allow notifications to be shown even when the lockscreen is in
- * "public" (secure & locked) mode?
- */
- public boolean userAllowsNotificationsInPublic(int userHandle) {
- if (userHandle == UserHandle.USER_ALL) {
- return true;
- }
-
- if (mUsersAllowingNotifications.indexOfKey(userHandle) < 0) {
- final boolean allowed = 0 != Settings.Secure.getIntForUser(
- mContext.getContentResolver(),
- Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, userHandle);
- mUsersAllowingNotifications.append(userHandle, allowed);
- return allowed;
- }
-
- return mUsersAllowingNotifications.get(userHandle);
- }
-
- /**
- * Has the given user chosen to allow their private (full) notifications to be shown even
- * when the lockscreen is in "public" (secure & locked) mode?
- */
- public boolean userAllowsPrivateNotificationsInPublic(int userHandle) {
- if (userHandle == UserHandle.USER_ALL) {
- return true;
- }
-
- if (mUsersAllowingPrivateNotifications.indexOfKey(userHandle) < 0) {
- final boolean allowedByUser = 0 != Settings.Secure.getIntForUser(
- mContext.getContentResolver(),
- Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle);
- final boolean allowedByDpm = adminAllowsUnredactedNotifications(userHandle);
- final boolean allowed = allowedByUser && allowedByDpm;
- mUsersAllowingPrivateNotifications.append(userHandle, allowed);
- return allowed;
- }
-
- return mUsersAllowingPrivateNotifications.get(userHandle);
- }
-
- private boolean adminAllowsUnredactedNotifications(int userHandle) {
- if (userHandle == UserHandle.USER_ALL) {
- return true;
- }
- final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */,
- userHandle);
- return (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS) == 0;
- }
-
- /**
- * Returns true if we're on a secure lockscreen and the user wants to hide notification data.
- * If so, notifications should be hidden.
- */
@Override // NotificationData.Environment
public boolean shouldHideNotifications(int userId) {
- return isLockscreenPublicMode(userId) && !userAllowsNotificationsInPublic(userId)
- || (userId != mCurrentUserId && shouldHideNotifications(mCurrentUserId));
+ return mLockscreenUserManager.shouldHideNotifications(userId);
}
- /**
- * Returns true if we're on a secure lockscreen and the user wants to hide notifications via
- * package-specific override.
- */
@Override // NotificationDate.Environment
public boolean shouldHideNotifications(String key) {
- return isLockscreenPublicMode(mCurrentUserId)
- && mNotificationData.getVisibilityOverride(key) == Notification.VISIBILITY_SECRET;
+ return mLockscreenUserManager.shouldHideNotifications(key);
}
/**
@@ -6205,7 +5127,7 @@ public class StatusBar extends SystemUI implements DemoMode,
*/
@Override // NotificationData.Environment
public boolean isSecurelyLocked(int userId) {
- return isLockscreenPublicMode(userId);
+ return mLockscreenUserManager.isLockscreenPublicMode(userId);
}
/**
@@ -6219,146 +5141,10 @@ public class StatusBar extends SystemUI implements DemoMode,
if (mState == StatusBarState.KEYGUARD) {
// Since the number of notifications is determined based on the height of the view, we
// need to update them.
- int maxBefore = getMaxKeyguardNotifications(false /* recompute */);
- int maxNotifications = getMaxKeyguardNotifications(true /* recompute */);
+ int maxBefore = getMaxNotificationsWhileLocked(false /* recompute */);
+ int maxNotifications = getMaxNotificationsWhileLocked(true /* recompute */);
if (maxBefore != maxNotifications) {
- updateRowStates();
- }
- }
- }
-
- protected void inflateViews(Entry entry, ViewGroup parent) {
- PackageManager pmUser = getPackageManagerForUser(mContext,
- entry.notification.getUser().getIdentifier());
-
- final StatusBarNotification sbn = entry.notification;
- if (entry.row != null) {
- entry.reset();
- updateNotification(entry, pmUser, sbn, entry.row);
- } else {
- new RowInflaterTask().inflate(mContext, parent, entry,
- row -> {
- bindRow(entry, pmUser, sbn, row);
- updateNotification(entry, pmUser, sbn, row);
- });
- }
-
- }
-
- private void bindRow(Entry entry, PackageManager pmUser,
- StatusBarNotification sbn, ExpandableNotificationRow row) {
- row.setExpansionLogger(this, entry.notification.getKey());
- row.setGroupManager(mGroupManager);
- row.setHeadsUpManager(mHeadsUpManager);
- row.setAboveShelfChangedListener(mAboveShelfObserver);
- row.setRemoteInputController(mRemoteInputController);
- row.setOnExpandClickListener(this);
- row.setRemoteViewClickHandler(mOnClickHandler);
- row.setInflationCallback(this);
- row.setSecureStateProvider(this::isKeyguardCurrentlySecure);
- row.setLongPressListener(getNotificationLongClicker());
-
- // Get the app name.
- // Note that Notification.Builder#bindHeaderAppName has similar logic
- // but since this field is used in the guts, it must be accurate.
- // Therefore we will only show the application label, or, failing that, the
- // package name. No substitutions.
- final String pkg = sbn.getPackageName();
- String appname = pkg;
- try {
- final ApplicationInfo info = pmUser.getApplicationInfo(pkg,
- PackageManager.MATCH_UNINSTALLED_PACKAGES
- | PackageManager.MATCH_DISABLED_COMPONENTS);
- if (info != null) {
- appname = String.valueOf(pmUser.getApplicationLabel(info));
- }
- } catch (NameNotFoundException e) {
- // Do nothing
- }
- row.setAppName(appname);
- row.setOnDismissRunnable(() ->
- performRemoveNotification(row.getStatusBarNotification()));
- row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
- if (ENABLE_REMOTE_INPUT) {
- row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
- }
- }
-
- private void updateNotification(Entry entry, PackageManager pmUser,
- StatusBarNotification sbn, ExpandableNotificationRow row) {
- row.setNeedsRedaction(needsRedaction(entry));
- boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey());
- boolean isUpdate = mNotificationData.get(entry.key) != null;
- boolean wasLowPriority = row.isLowPriority();
- row.setIsLowPriority(isLowPriority);
- row.setLowPriorityStateUpdated(isUpdate && (wasLowPriority != isLowPriority));
- // bind the click event to the content area
- mNotificationClicker.register(row, sbn);
-
- // Extract target SDK version.
- try {
- ApplicationInfo info = pmUser.getApplicationInfo(sbn.getPackageName(), 0);
- entry.targetSdk = info.targetSdkVersion;
- } catch (NameNotFoundException ex) {
- Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex);
- }
- row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD
- && entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
- entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
- entry.autoRedacted = entry.notification.getNotification().publicVersion == null;
-
- entry.row = row;
- entry.row.setOnActivatedListener(this);
-
- boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(sbn,
- mNotificationData.getImportance(sbn.getKey()));
- boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight && mPanelExpanded;
- row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
- row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
- row.updateNotification(entry);
- }
-
- /**
- * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this
- * via first-class API.
- *
- * TODO: Remove once enough apps specify remote inputs on their own.
- */
- private void processForRemoteInput(Notification n) {
- if (!ENABLE_REMOTE_INPUT) return;
-
- if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") &&
- (n.actions == null || n.actions.length == 0)) {
- Notification.Action viableAction = null;
- Notification.WearableExtender we = new Notification.WearableExtender(n);
-
- List<Notification.Action> actions = we.getActions();
- final int numActions = actions.size();
-
- for (int i = 0; i < numActions; i++) {
- Notification.Action action = actions.get(i);
- if (action == null) {
- continue;
- }
- RemoteInput[] remoteInputs = action.getRemoteInputs();
- if (remoteInputs == null) {
- continue;
- }
- for (RemoteInput ri : remoteInputs) {
- if (ri.getAllowFreeFormInput()) {
- viableAction = action;
- break;
- }
- }
- if (viableAction != null) {
- break;
- }
- }
-
- if (viableAction != null) {
- Notification.Builder rebuilder = Notification.Builder.recoverBuilder(mContext, n);
- rebuilder.setActions(viableAction);
- rebuilder.build(); // will rewrite n
+ mViewHierarchyManager.updateRowStates();
}
}
}
@@ -6368,7 +5154,7 @@ public class StatusBar extends SystemUI implements DemoMode,
final boolean afterKeyguardGone = intent.isActivity()
&& PreviewInflater.wouldLaunchResolverActivity(mContext, intent.getIntent(),
- mCurrentUserId);
+ mLockscreenUserManager.getCurrentUserId());
dismissKeyguardThenExecute(() -> {
new Thread(() -> {
try {
@@ -6406,166 +5192,15 @@ public class StatusBar extends SystemUI implements DemoMode,
}, afterKeyguardGone);
}
-
- private final class NotificationClicker implements View.OnClickListener {
-
- @Override
- public void onClick(final View v) {
- if (!(v instanceof ExpandableNotificationRow)) {
- Log.e(TAG, "NotificationClicker called on a view that is not a notification row.");
- return;
- }
-
- wakeUpIfDozing(SystemClock.uptimeMillis(), v);
-
- final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
- final StatusBarNotification sbn = row.getStatusBarNotification();
- if (sbn == null) {
- Log.e(TAG, "NotificationClicker called on an unclickable notification,");
- return;
- }
-
- // Check if the notification is displaying the menu, if so slide notification back
- if (row.getProvider() != null && row.getProvider().isMenuVisible()) {
- row.animateTranslateNotification(0);
- return;
- }
-
- Notification notification = sbn.getNotification();
- final PendingIntent intent = notification.contentIntent != null
- ? notification.contentIntent
- : notification.fullScreenIntent;
- final String notificationKey = sbn.getKey();
-
- // Mark notification for one frame.
- row.setJustClicked(true);
- DejankUtils.postAfterTraversal(() -> row.setJustClicked(false));
-
- final boolean afterKeyguardGone = intent.isActivity()
- && PreviewInflater.wouldLaunchResolverActivity(mContext, intent.getIntent(),
- mCurrentUserId);
- dismissKeyguardThenExecute(() -> {
- if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUp(notificationKey)) {
- // Release the HUN notification to the shade.
-
- if (isPresenterFullyCollapsed()) {
- HeadsUpManager.setIsClickedNotification(row, true);
- }
- //
- // In most cases, when FLAG_AUTO_CANCEL is set, the notification will
- // become canceled shortly by NoMan, but we can't assume that.
- mHeadsUpManager.releaseImmediately(notificationKey);
- }
- StatusBarNotification parentToCancel = null;
- if (shouldAutoCancel(sbn) && mGroupManager.isOnlyChildInGroup(sbn)) {
- StatusBarNotification summarySbn =
- mGroupManager.getLogicalGroupSummary(sbn).getStatusBarNotification();
- if (shouldAutoCancel(summarySbn)) {
- parentToCancel = summarySbn;
- }
- }
- final StatusBarNotification parentToCancelFinal = parentToCancel;
- final Runnable runnable = () -> {
- try {
- // The intent we are sending is for the application, which
- // won't have permission to immediately start an activity after
- // the user switches to home. We know it is safe to do at this
- // point, so make sure new activity switches are now allowed.
- ActivityManager.getService().resumeAppSwitches();
- } catch (RemoteException e) {
- }
- if (intent != null) {
- // If we are launching a work activity and require to launch
- // separate work challenge, we defer the activity action and cancel
- // notification until work challenge is unlocked.
- if (intent.isActivity()) {
- final int userId = intent.getCreatorUserHandle().getIdentifier();
- if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)
- && mKeyguardManager.isDeviceLocked(userId)) {
- // TODO(b/28935539): should allow certain activities to
- // bypass work challenge
- if (startWorkChallengeIfNecessary(userId, intent.getIntentSender(),
- notificationKey)) {
- // Show work challenge, do not run PendingIntent and
- // remove notification
- return;
- }
- }
- }
- try {
- intent.send(null, 0, null, null, null, null, getActivityOptions());
- } catch (PendingIntent.CanceledException e) {
- // the stack trace isn't very helpful here.
- // Just log the exception message.
- Log.w(TAG, "Sending contentIntent failed: " + e);
-
- // TODO: Dismiss Keyguard.
- }
- if (intent.isActivity()) {
- mAssistManager.hideAssist();
- }
- }
-
- try {
- mBarService.onNotificationClick(notificationKey);
- } catch (RemoteException ex) {
- // system process is dead if we're here.
- }
- if (parentToCancelFinal != null) {
- // We have to post it to the UI thread for synchronization
- mHandler.post(() -> {
- Runnable removeRunnable =
- () -> performRemoveNotification(parentToCancelFinal);
- if (isCollapsing()) {
- // To avoid lags we're only performing the remove
- // after the shade was collapsed
- addPostCollapseAction(removeRunnable);
- } else {
- removeRunnable.run();
- }
- });
- }
- };
-
- if (mStatusBarKeyguardViewManager.isShowing()
- && mStatusBarKeyguardViewManager.isOccluded()) {
- mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
- } else {
- new Thread(runnable).start();
- }
-
- if (!mNotificationPanel.isFullyCollapsed()) {
- // close the shade if it was open
- animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */,
- true /* delayed */);
- visibilityChanged(false);
-
- return true;
- } else {
- return false;
- }
- }, afterKeyguardGone);
- }
-
- private boolean shouldAutoCancel(StatusBarNotification sbn) {
- int flags = sbn.getNotification().flags;
- if ((flags & Notification.FLAG_AUTO_CANCEL) != Notification.FLAG_AUTO_CANCEL) {
- return false;
- }
- if ((flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
- return false;
- }
- return true;
+ private boolean shouldAutoCancel(StatusBarNotification sbn) {
+ int flags = sbn.getNotification().flags;
+ if ((flags & Notification.FLAG_AUTO_CANCEL) != Notification.FLAG_AUTO_CANCEL) {
+ return false;
}
-
- public void register(ExpandableNotificationRow row, StatusBarNotification sbn) {
- Notification notification = sbn.getNotification();
- if (notification.contentIntent != null || notification.fullScreenIntent != null) {
- row.setOnClickListener(this);
- } else {
- row.setOnClickListener(null);
- }
+ if ((flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
+ return false;
}
+ return true;
}
protected Bundle getActivityOptions() {
@@ -6608,125 +5243,10 @@ public class StatusBar extends SystemUI implements DemoMode,
}
/**
- * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService
- * about the failure.
- *
- * WARNING: this will call back into us. Don't hold any locks.
- */
- void handleNotificationError(StatusBarNotification n, String message) {
- removeNotification(n.getKey(), null);
- try {
- mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(),
- n.getInitialPid(), message, n.getUserId());
- } catch (RemoteException ex) {
- // The end is nigh.
- }
- }
-
- protected StatusBarNotification removeNotificationViews(String key, RankingMap ranking) {
- NotificationData.Entry entry = mNotificationData.remove(key, ranking);
- if (entry == null) {
- Log.w(TAG, "removeNotification for unknown key: " + key);
- return null;
- }
- updateNotifications();
- Dependency.get(LeakDetector.class).trackGarbage(entry);
- return entry.notification;
- }
-
- protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn)
- throws InflationException {
- if (DEBUG) {
- Log.d(TAG, "createNotificationViews(notification=" + sbn);
- }
- NotificationData.Entry entry = new NotificationData.Entry(sbn);
- Dependency.get(LeakDetector.class).trackInstance(entry);
- entry.createIcons(mContext, sbn);
- // Construct the expanded view.
- inflateViews(entry, mStackScroller);
- return entry;
- }
-
- protected void addNotificationViews(Entry entry) {
- if (entry == null) {
- return;
- }
- // Add the expanded view and icon.
- mNotificationData.add(entry);
- updateNotifications();
- }
-
- /**
* Updates expanded, dimmed and locked states of notification rows.
*/
- protected void updateRowStates() {
- final int N = mStackScroller.getChildCount();
-
- int visibleNotifications = 0;
- boolean onKeyguard = mState == StatusBarState.KEYGUARD;
- int maxNotifications = -1;
- if (onKeyguard) {
- maxNotifications = getMaxKeyguardNotifications(true /* recompute */);
- }
- mStackScroller.setMaxDisplayedNotifications(maxNotifications);
- Stack<ExpandableNotificationRow> stack = new Stack<>();
- for (int i = N - 1; i >= 0; i--) {
- View child = mStackScroller.getChildAt(i);
- if (!(child instanceof ExpandableNotificationRow)) {
- continue;
- }
- stack.push((ExpandableNotificationRow) child);
- }
- while(!stack.isEmpty()) {
- ExpandableNotificationRow row = stack.pop();
- NotificationData.Entry entry = row.getEntry();
- boolean isChildNotification =
- mGroupManager.isChildInGroupWithSummary(entry.notification);
-
- row.setOnKeyguard(onKeyguard);
-
- if (!onKeyguard) {
- // If mAlwaysExpandNonGroupedNotification is false, then only expand the
- // very first notification and if it's not a child of grouped notifications.
- row.setSystemExpanded(mAlwaysExpandNonGroupedNotification
- || (visibleNotifications == 0 && !isChildNotification
- && !row.isLowPriority()));
- }
-
- entry.row.setShowAmbient(isDozing());
- int userId = entry.notification.getUserId();
- boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
- entry.notification) && !entry.row.isRemoved();
- boolean showOnKeyguard = shouldShowOnKeyguard(entry.notification);
- if (suppressedSummary
- || (isLockscreenPublicMode(userId) && !mShowLockscreenNotifications)
- || (onKeyguard && !showOnKeyguard)) {
- entry.row.setVisibility(View.GONE);
- } else {
- boolean wasGone = entry.row.getVisibility() == View.GONE;
- if (wasGone) {
- entry.row.setVisibility(View.VISIBLE);
- }
- if (!isChildNotification && !entry.row.isRemoved()) {
- if (wasGone) {
- // notify the scroller of a child addition
- mStackScroller.generateAddAnimation(entry.row,
- !showOnKeyguard /* fromMoreCard */);
- }
- visibleNotifications++;
- }
- }
- if (row.isSummaryWithChildren()) {
- List<ExpandableNotificationRow> notificationChildren =
- row.getNotificationChildren();
- int size = notificationChildren.size();
- for (int i = size - 1; i >= 0; i--) {
- stack.push(notificationChildren.get(i));
- }
- }
- }
- mNotificationPanel.setNoVisibleNotifications(visibleNotifications == 0);
-
+ @Override
+ public void onUpdateRowStates() {
// The following views will be moved to the end of mStackScroller. This counter represents
// the offset from the last child. Initialized to 1 for the very last position. It is post-
// incremented in the following "changeViewPosition" calls so that its value is correct for
@@ -6749,191 +5269,10 @@ public class StatusBar extends SystemUI implements DemoMode,
mScrimController.setNotificationCount(mStackScroller.getNotGoneChildCount());
}
- public boolean shouldShowOnKeyguard(StatusBarNotification sbn) {
- return mShowLockscreenNotifications
- && ((mDisabled2 & DISABLE2_NOTIFICATION_SHADE) == 0)
- && !mNotificationData.isAmbient(sbn.getKey());
- }
-
- // extended in StatusBar
- protected void setShowLockscreenNotifications(boolean show) {
- mShowLockscreenNotifications = show;
- }
-
- protected void setLockScreenAllowRemoteInput(boolean allowLockscreenRemoteInput) {
- mAllowLockscreenRemoteInput = allowLockscreenRemoteInput;
- }
-
- protected void updateLockscreenNotificationSetting() {
- final boolean show = Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
- 1,
- mCurrentUserId) != 0;
- final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(
- null /* admin */, mCurrentUserId);
- final boolean allowedByDpm = (dpmFlags
- & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0;
-
- setShowLockscreenNotifications(show && allowedByDpm);
-
- if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
- final boolean remoteInput = Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT,
- 0,
- mCurrentUserId) != 0;
- final boolean remoteInputDpm =
- (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_REMOTE_INPUT) == 0;
-
- setLockScreenAllowRemoteInput(remoteInput && remoteInputDpm);
- } else {
- setLockScreenAllowRemoteInput(false);
- }
- }
-
- public void updateNotification(StatusBarNotification notification, RankingMap ranking)
- throws InflationException {
- if (DEBUG) Log.d(TAG, "updateNotification(" + notification + ")");
-
- final String key = notification.getKey();
- abortExistingInflation(key);
- Entry entry = mNotificationData.get(key);
- if (entry == null) {
- return;
- }
- mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
- mRemoteInputEntriesToRemoveOnCollapse.remove(entry);
- if (key.equals(mGutsManager.getKeyToRemoveOnGutsClosed())) {
- mGutsManager.setKeyToRemoveOnGutsClosed(null);
- Log.w(TAG, "Notification that was kept for guts was updated. " + key);
- }
-
- Notification n = notification.getNotification();
- mNotificationData.updateRanking(ranking);
-
- final StatusBarNotification oldNotification = entry.notification;
- entry.notification = notification;
- mGroupManager.onEntryUpdated(entry, oldNotification);
-
- entry.updateIcons(mContext, notification);
- inflateViews(entry, mStackScroller);
-
- mForegroundServiceController.updateNotification(notification,
- mNotificationData.getImportance(key));
-
- boolean shouldPeek = shouldPeek(entry, notification);
- boolean alertAgain = alertAgain(entry, n);
-
- updateHeadsUp(key, entry, shouldPeek, alertAgain);
- updateNotifications();
-
- if (!notification.isClearable()) {
- // The user may have performed a dismiss action on the notification, since it's
- // not clearable we should snap it back.
- mStackScroller.snapViewIfNeeded(entry.row);
- }
-
- if (DEBUG) {
- // Is this for you?
- boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
- Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
- }
-
- setAreThereNotifications();
- }
-
protected void notifyHeadsUpGoingToSleep() {
maybeEscalateHeadsUp();
}
- private boolean alertAgain(Entry oldEntry, Notification newNotification) {
- return oldEntry == null || !oldEntry.hasInterrupted()
- || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0;
- }
-
- protected boolean shouldPeek(Entry entry) {
- return shouldPeek(entry, entry.notification);
- }
-
- protected boolean shouldPeek(Entry entry, StatusBarNotification sbn) {
- if (!mUseHeadsUp || isDeviceInVrMode()) {
- if (DEBUG) Log.d(TAG, "No peeking: no huns or vr mode");
- return false;
- }
-
- if (mNotificationData.shouldFilterOut(sbn)) {
- if (DEBUG) Log.d(TAG, "No peeking: filtered notification: " + sbn.getKey());
- return false;
- }
-
- boolean inUse = mPowerManager.isScreenOn() && !mSystemServicesProxy.isDreaming();
-
- if (!inUse && !isDozing()) {
- if (DEBUG) {
- Log.d(TAG, "No peeking: not in use: " + sbn.getKey());
- }
- return false;
- }
-
- if (!isDozing() && mNotificationData.shouldSuppressScreenOn(sbn.getKey())) {
- if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
- return false;
- }
-
- if (isDozing() && mNotificationData.shouldSuppressScreenOff(sbn.getKey())) {
- if (DEBUG) Log.d(TAG, "No peeking: suppressed by DND: " + sbn.getKey());
- return false;
- }
-
- if (entry.hasJustLaunchedFullScreenIntent()) {
- if (DEBUG) Log.d(TAG, "No peeking: recent fullscreen: " + sbn.getKey());
- return false;
- }
-
- if (isSnoozedPackage(sbn)) {
- if (DEBUG) Log.d(TAG, "No peeking: snoozed package: " + sbn.getKey());
- return false;
- }
-
- // Allow peeking for DEFAULT notifications only if we're on Ambient Display.
- int importanceLevel = isDozing() ? NotificationManager.IMPORTANCE_DEFAULT
- : NotificationManager.IMPORTANCE_HIGH;
- if (mNotificationData.getImportance(sbn.getKey()) < importanceLevel) {
- if (DEBUG) Log.d(TAG, "No peeking: unimportant notification: " + sbn.getKey());
- return false;
- }
-
- if (mIsOccluded && !isDozing()) {
- boolean devicePublic = isLockscreenPublicMode(mCurrentUserId);
- boolean userPublic = devicePublic || isLockscreenPublicMode(sbn.getUserId());
- boolean needsRedaction = needsRedaction(entry);
- if (userPublic && needsRedaction) {
- return false;
- }
- }
-
- if (sbn.getNotification().fullScreenIntent != null) {
- if (mAccessibilityManager.isTouchExplorationEnabled()) {
- if (DEBUG) Log.d(TAG, "No peeking: accessible fullscreen: " + sbn.getKey());
- return false;
- } else if (mDozing) {
- // We never want heads up when we are dozing.
- return false;
- } else {
- // we only allow head-up on the lockscreen if it doesn't have a fullscreen intent
- return !mStatusBarKeyguardViewManager.isShowing()
- || mStatusBarKeyguardViewManager.isOccluded();
- }
- }
-
- // Don't peek notifications that are suppressed due to group alert behavior
- if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
- if (DEBUG) Log.d(TAG, "No peeking: suppressed due to group alert behavior");
- return false;
- }
-
- return true;
- }
-
/**
* @return Whether the security bouncer from Keyguard is showing.
*/
@@ -6963,17 +5302,6 @@ public class StatusBar extends SystemUI implements DemoMode,
return contextForUser.getPackageManager();
}
- @Override
- public void logNotificationExpansion(String key, boolean userAction, boolean expanded) {
- mUiOffloadThread.submit(() -> {
- try {
- mBarService.onNotificationExpansionChanged(key, userAction, expanded);
- } catch (RemoteException e) {
- // Ignore.
- }
- });
- }
-
public boolean isKeyguardSecure() {
if (mStatusBarKeyguardViewManager == null) {
// startKeyguard() hasn't been called yet, so we don't know.
@@ -7021,26 +5349,16 @@ public class StatusBar extends SystemUI implements DemoMode,
}
@Override
- public int getCurrentUserId() {
- return mCurrentUserId;
- }
-
- @Override
- public NotificationData getNotificationData() {
- return mNotificationData;
- }
-
- @Override
- public RankingMap getLatestRankingMap() {
- return mLatestRankingMap;
+ public Handler getHandler() {
+ return mHandler;
}
- final NotificationInfo.CheckSaveListener mCheckSaveListener =
+ private final NotificationInfo.CheckSaveListener mCheckSaveListener =
(Runnable saveImportance, StatusBarNotification sbn) -> {
// If the user has security enabled, show challenge if the setting is changed.
- if (isLockscreenPublicMode(sbn.getUser().getIdentifier()) && (
- mState == StatusBarState.KEYGUARD
- || mState == StatusBarState.SHADE_LOCKED)) {
+ if (mLockscreenUserManager.isLockscreenPublicMode(sbn.getUser().getIdentifier())
+ && (mState == StatusBarState.KEYGUARD ||
+ mState == StatusBarState.SHADE_LOCKED)) {
onLockedNotificationImportanceChange(() -> {
saveImportance.run();
return true;
diff --git a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index ef05bbb4..8504d8e5 100644
--- a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -33,7 +33,7 @@ import android.view.WindowManagerGlobal;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.keyguard.LatencyTracker;
+import com.android.internal.util.LatencyTracker;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.DejankUtils;
import com.android.systemui.Dependency;
@@ -380,8 +380,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
mStatusBar.fadeKeyguardWhilePulsing();
wakeAndUnlockDejank();
} else {
- mFingerprintUnlockController.startKeyguardFadingAway();
- mStatusBar.setKeyguardFadingAway(startTime, delay, fadeoutDuration);
boolean staying = mStatusBar.hideKeyguard();
if (!staying) {
mStatusBarWindowManager.setKeyguardFadingAway(true);
diff --git a/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
index ed96b411..c30f6339 100644
--- a/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
+++ b/com/android/systemui/statusbar/phone/StatusBarWindowManager.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.phone;
+import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT;
+
import android.app.ActivityManager;
import android.app.IActivityManager;
import android.content.Context;
@@ -52,6 +54,7 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D
private final Context mContext;
private final WindowManager mWindowManager;
private final IActivityManager mActivityManager;
+ private final DozeParameters mDozeParameters;
private View mStatusBarView;
private WindowManager.LayoutParams mLp;
private WindowManager.LayoutParams mLpChanged;
@@ -68,8 +71,8 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mActivityManager = ActivityManager.getService();
mKeyguardScreenRotation = shouldEnableKeyguardScreenRotation();
- mScreenBrightnessDoze = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_screenBrightnessDoze) / 255f;
+ mDozeParameters = new DozeParameters(mContext);
+ mScreenBrightnessDoze = mDozeParameters.getScreenBrightnessDoze();
}
private boolean shouldEnableKeyguardScreenRotation() {
@@ -134,7 +137,11 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D
mLpChanged.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
}
- if (state.keyguardShowing && !state.backdropShowing && !state.dozing) {
+ final boolean showWallpaperOnAod = mDozeParameters.getAlwaysOn() &&
+ state.wallpaperSupportsAmbientMode &&
+ state.scrimsVisibility != ScrimController.VISIBILITY_FULLY_OPAQUE;
+ if (state.keyguardShowing && !state.backdropShowing &&
+ (!state.dozing || showWallpaperOnAod)) {
mLpChanged.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
} else {
mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
@@ -156,7 +163,7 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D
private void applyFocusableFlag(State state) {
boolean panelFocusable = state.statusBarFocusable && state.panelExpanded;
if (state.bouncerShowing && (state.keyguardOccluded || state.keyguardNeedsInput)
- || StatusBar.ENABLE_REMOTE_INPUT && state.remoteInputActive) {
+ || ENABLE_REMOTE_INPUT && state.remoteInputActive) {
mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
} else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) {
@@ -186,7 +193,8 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D
private boolean isExpanded(State state) {
return !state.forceCollapsed && (state.isKeyguardShowingAndNotOccluded()
|| state.panelVisible || state.keyguardFadingAway || state.bouncerShowing
- || state.headsUpShowing || state.scrimsVisible);
+ || state.headsUpShowing
+ || state.scrimsVisibility != ScrimController.VISIBILITY_FULLY_TRANSPARENT);
}
private void applyFitsSystemWindows(State state) {
@@ -334,8 +342,8 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D
apply(mCurrentState);
}
- public void setScrimsVisible(boolean scrimsVisible) {
- mCurrentState.scrimsVisible = scrimsVisible;
+ public void setScrimsVisibility(int scrimsVisibility) {
+ mCurrentState.scrimsVisibility = scrimsVisibility;
apply(mCurrentState);
}
@@ -344,6 +352,11 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D
apply(mCurrentState);
}
+ public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
+ mCurrentState.wallpaperSupportsAmbientMode = supportsAmbientMode;
+ apply(mCurrentState);
+ }
+
/**
* @param state The {@link StatusBarState} of the status bar.
*/
@@ -431,6 +444,7 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D
boolean forceDozeBrightness;
boolean forceUserActivity;
boolean backdropShowing;
+ boolean wallpaperSupportsAmbientMode;
/**
* The {@link StatusBar} state from the status bar.
@@ -440,7 +454,7 @@ public class StatusBarWindowManager implements RemoteInputController.Callback, D
boolean remoteInputActive;
boolean forcePluginOpen;
boolean dozing;
- boolean scrimsVisible;
+ int scrimsVisibility;
private boolean isKeyguardShowingAndNotOccluded() {
return keyguardShowing && !keyguardOccluded;
diff --git a/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/com/android/systemui/statusbar/phone/StatusBarWindowView.java
index d7f11f71..4accd86c 100644
--- a/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -248,7 +248,6 @@ public class StatusBarWindowView extends FrameLayout {
public void setTouchActive(boolean touchActive) {
mTouchActive = touchActive;
- mStackScrollLayout.setTouchActive(touchActive);
}
@Override
diff --git a/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/com/android/systemui/statusbar/policy/LocationControllerImpl.java
index 874f0d9d..4ee4ef49 100644
--- a/com/android/systemui/statusbar/policy/LocationControllerImpl.java
+++ b/com/android/systemui/statusbar/policy/LocationControllerImpl.java
@@ -39,6 +39,8 @@ import com.android.systemui.util.Utils;
import java.util.ArrayList;
import java.util.List;
+import static com.android.settingslib.Utils.updateLocationMode;
+
/**
* A controller to manage changes of location related states and update the views accordingly.
*/
@@ -106,12 +108,13 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio
final ContentResolver cr = mContext.getContentResolver();
// When enabling location, a user consent dialog will pop up, and the
// setting won't be fully enabled until the user accepts the agreement.
+ int currentMode = Settings.Secure.getIntForUser(cr, Settings.Secure.LOCATION_MODE,
+ Settings.Secure.LOCATION_MODE_OFF, currentUserId);
int mode = enabled
? Settings.Secure.LOCATION_MODE_PREVIOUS : Settings.Secure.LOCATION_MODE_OFF;
// QuickSettings always runs as the owner, so specifically set the settings
// for the current foreground user.
- return Settings.Secure
- .putIntForUser(cr, Settings.Secure.LOCATION_MODE, mode, currentUserId);
+ return updateLocationMode(mContext, currentMode, mode, currentUserId);
}
/**
diff --git a/com/android/systemui/statusbar/policy/MobileSignalController.java b/com/android/systemui/statusbar/policy/MobileSignalController.java
index 652f8bb6..8516278a 100644
--- a/com/android/systemui/statusbar/policy/MobileSignalController.java
+++ b/com/android/systemui/statusbar/policy/MobileSignalController.java
@@ -288,7 +288,7 @@ public class MobileSignalController extends SignalController<
String description = null;
// Only send data sim callbacks to QS.
if (mCurrentState.dataSim) {
- qsTypeIcon = showDataIcon ? icons.mQsDataType : 0;
+ qsTypeIcon = (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.mQsDataType : 0;
qsIcon = new IconState(mCurrentState.enabled
&& !mCurrentState.isEmergency, getQsCurrentIconId(), contentDescription);
description = mCurrentState.isEmergency ? null : mCurrentState.networkName;
@@ -300,7 +300,7 @@ public class MobileSignalController extends SignalController<
&& !mCurrentState.carrierNetworkChangeMode
&& mCurrentState.activityOut;
showDataIcon &= mCurrentState.isDefault || dataDisabled;
- int typeIcon = showDataIcon ? icons.mDataType : 0;
+ int typeIcon = (showDataIcon || mConfig.alwaysShowDataRatIcon) ? icons.mDataType : 0;
callback.setMobileDataIndicators(statusIcon, qsIcon, typeIcon, qsTypeIcon,
activityIn, activityOut, dataContentDescription, description, icons.mIsWide,
mSubscriptionInfo.getSubscriptionId(), mCurrentState.roaming);
@@ -460,7 +460,7 @@ public class MobileSignalController extends SignalController<
mCurrentState.roaming = isRoaming();
if (isCarrierNetworkChangeActive()) {
mCurrentState.iconGroup = TelephonyIcons.CARRIER_NETWORK_CHANGE;
- } else if (isDataDisabled()) {
+ } else if (isDataDisabled() && !mConfig.alwaysShowDataRatIcon) {
mCurrentState.iconGroup = TelephonyIcons.DATA_DISABLED;
}
if (isEmergencyOnly() != mCurrentState.isEmergency) {
diff --git a/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
index 40ee8386..baf0ebf5 100644
--- a/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
+++ b/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -29,7 +29,9 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.os.PersistableBundle;
import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import android.telephony.SubscriptionInfo;
@@ -245,6 +247,7 @@ public class NetworkControllerImpl extends BroadcastReceiver
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(ConnectivityManager.INET_CONDITION_ACTION);
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
mContext.registerReceiver(this, filter, null, mReceiverHandler);
mListening = true;
@@ -426,6 +429,14 @@ public class NetworkControllerImpl extends BroadcastReceiver
// emergency state.
recalculateEmergency();
}
+ } else if (action.equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+ mConfig = Config.readConfig(mContext);
+ mReceiverHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ handleConfigurationChanged();
+ }
+ });
} else {
int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
@@ -969,6 +980,7 @@ public class NetworkControllerImpl extends BroadcastReceiver
boolean hideLtePlus = false;
boolean hspaDataDistinguishable;
boolean inflateSignalStrengths = false;
+ boolean alwaysShowDataRatIcon = false;
static Config readConfig(Context context) {
Config config = new Config();
@@ -982,6 +994,14 @@ public class NetworkControllerImpl extends BroadcastReceiver
res.getBoolean(R.bool.config_hspa_data_distinguishable);
config.hideLtePlus = res.getBoolean(R.bool.config_hideLtePlus);
config.inflateSignalStrengths = res.getBoolean(R.bool.config_inflateSignalStrength);
+
+ CarrierConfigManager configMgr = (CarrierConfigManager)
+ context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ PersistableBundle b = configMgr.getConfig();
+ if (b != null) {
+ config.alwaysShowDataRatIcon = b.getBoolean(
+ CarrierConfigManager.KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL);
+ }
return config;
}
}
diff --git a/com/android/systemui/statusbar/policy/RotationLockController.java b/com/android/systemui/statusbar/policy/RotationLockController.java
index 722874b0..f258fb19 100644
--- a/com/android/systemui/statusbar/policy/RotationLockController.java
+++ b/com/android/systemui/statusbar/policy/RotationLockController.java
@@ -24,6 +24,7 @@ public interface RotationLockController extends Listenable,
boolean isRotationLockAffordanceVisible();
boolean isRotationLocked();
void setRotationLocked(boolean locked);
+ void setRotationLockedAtAngle(boolean locked, int rotation);
public interface RotationLockControllerCallback {
void onRotationLockStateChanged(boolean rotationLocked, boolean affordanceVisible);
diff --git a/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java b/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
index 4f964964..5418dc14 100644
--- a/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
+++ b/com/android/systemui/statusbar/policy/RotationLockControllerImpl.java
@@ -63,6 +63,10 @@ public final class RotationLockControllerImpl implements RotationLockController
RotationPolicy.setRotationLock(mContext, locked);
}
+ public void setRotationLockedAtAngle(boolean locked, int rotation){
+ RotationPolicy.setRotationLockAtAngle(mContext, locked, rotation);
+ }
+
public boolean isRotationLockAffordanceVisible() {
return RotationPolicy.isRotationLockToggleVisible(mContext);
}
diff --git a/com/android/systemui/statusbar/stack/ExpandableViewState.java b/com/android/systemui/statusbar/stack/ExpandableViewState.java
index e0fd481c..0650e23d 100644
--- a/com/android/systemui/statusbar/stack/ExpandableViewState.java
+++ b/com/android/systemui/statusbar/stack/ExpandableViewState.java
@@ -95,6 +95,12 @@ public class ExpandableViewState extends ViewState {
public boolean inShelf;
/**
+ * A state indicating whether a headsup is currently fully visible, even when not scrolled.
+ * Only valid if the view is heads upped.
+ */
+ public boolean headsUpIsVisible;
+
+ /**
* How much the child overlaps with the previous child on top. This is used to
* show the background properly when the child on top is translating away.
*/
@@ -126,6 +132,7 @@ public class ExpandableViewState extends ViewState {
clipTopAmount = svs.clipTopAmount;
notGoneIndex = svs.notGoneIndex;
location = svs.location;
+ headsUpIsVisible = svs.headsUpIsVisible;
}
}
@@ -175,6 +182,10 @@ public class ExpandableViewState extends ViewState {
expandableView.setTransformingInShelf(false);
expandableView.setInShelf(inShelf);
+
+ if (headsUpIsVisible) {
+ expandableView.setHeadsUpIsVisible();
+ }
}
}
@@ -229,6 +240,10 @@ public class ExpandableViewState extends ViewState {
expandableView.setTransformingInShelf(true);
}
expandableView.setInShelf(this.inShelf);
+
+ if (headsUpIsVisible) {
+ expandableView.setHeadsUpIsVisible();
+ }
}
private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) {
diff --git a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index ebebfacf..369e7ffa 100644
--- a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -80,6 +80,8 @@ import com.android.systemui.statusbar.ExpandableOutlineView;
import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.NotificationGuts;
+import com.android.systemui.statusbar.NotificationListContainer;
+import com.android.systemui.statusbar.NotificationLogger;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.NotificationSnooze;
import com.android.systemui.statusbar.StackScrollerDecorView;
@@ -112,7 +114,8 @@ import java.util.List;
public class NotificationStackScrollLayout extends ViewGroup
implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener,
- NotificationMenuRowPlugin.OnMenuEventListener, VisibilityLocationProvider {
+ NotificationMenuRowPlugin.OnMenuEventListener, VisibilityLocationProvider,
+ NotificationListContainer {
public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
private static final String TAG = "StackScroller";
@@ -207,7 +210,7 @@ public class NotificationStackScrollLayout extends ViewGroup
* The raw amount of the overScroll on the bottom, which is not rubber-banded.
*/
private float mOverScrolledBottomPixels;
- private OnChildLocationsChangedListener mListener;
+ private NotificationLogger.OnChildLocationsChangedListener mListener;
private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
private OnEmptySpaceClickListener mOnEmptySpaceClickListener;
@@ -447,6 +450,7 @@ public class NotificationStackScrollLayout extends ViewGroup
}
}
+ @Override
public NotificationSwipeActionHelper getSwipeActionHelper() {
return mSwipeHelper;
}
@@ -614,7 +618,9 @@ public class NotificationStackScrollLayout extends ViewGroup
mNoAmbient = noAmbient;
}
- public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
+ @Override
+ public void setChildLocationsChangedListener(
+ NotificationLogger.OnChildLocationsChangedListener listener) {
mListener = listener;
}
@@ -624,7 +630,7 @@ public class NotificationStackScrollLayout extends ViewGroup
if (childViewState == null) {
return false;
}
- if ((childViewState.location &= ExpandableViewState.VISIBLE_LOCATIONS) == 0) {
+ if ((childViewState.location & ExpandableViewState.VISIBLE_LOCATIONS) == 0) {
return false;
}
if (row.getVisibility() != View.VISIBLE) {
@@ -1325,6 +1331,7 @@ public class NotificationStackScrollLayout extends ViewGroup
true /* isDismissAll */);
}
+ @Override
public void snapViewIfNeeded(ExpandableNotificationRow child) {
boolean animate = mIsExpanded || isPinnedHeadsUp(child);
// If the child is showing the notification menu snap to that
@@ -1333,6 +1340,11 @@ public class NotificationStackScrollLayout extends ViewGroup
}
@Override
+ public ViewGroup getViewParentForNotification(NotificationData.Entry entry) {
+ return this;
+ }
+
+ @Override
public boolean onTouchEvent(MotionEvent ev) {
boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
|| ev.getActionMasked()== MotionEvent.ACTION_UP;
@@ -2053,6 +2065,7 @@ public class NotificationStackScrollLayout extends ViewGroup
return mAmbientState.isPulsing(entry);
}
+ @Override
public boolean hasPulsingNotifications() {
return mPulsing != null;
}
@@ -2610,10 +2623,7 @@ public class NotificationStackScrollLayout extends ViewGroup
}
}
- /**
- * Called when a notification is removed from the shade. This cleans up the state for a given
- * view.
- */
+ @Override
public void cleanUpViewState(View child) {
if (child == mTranslatingParentView) {
mTranslatingParentView = null;
@@ -2922,10 +2932,12 @@ public class NotificationStackScrollLayout extends ViewGroup
}
}
+ @Override
public void notifyGroupChildRemoved(View row, ViewGroup childrenContainer) {
onViewRemovedInternal(row, childrenContainer);
}
+ @Override
public void notifyGroupChildAdded(View row) {
onViewAddedInternal(row);
}
@@ -2963,12 +2975,8 @@ public class NotificationStackScrollLayout extends ViewGroup
return mNeedsAnimation
&& (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
}
- /**
- * Generate an animation for an added child view.
- *
- * @param child The view to be added.
- * @param fromMoreCard Whether this add is coming from the "more" card on lockscreen.
- */
+
+ @Override
public void generateAddAnimation(View child, boolean fromMoreCard) {
if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) {
// Generate Animations
@@ -2984,12 +2992,7 @@ public class NotificationStackScrollLayout extends ViewGroup
}
}
- /**
- * Change the position of child to a new location
- *
- * @param child the view to change the position for
- * @param newIndex the new index
- */
+ @Override
public void changeViewPosition(View child, int newIndex) {
int currentIndex = indexOfChild(child);
if (child != null && child.getParent() == this && currentIndex != newIndex) {
@@ -3681,6 +3684,7 @@ public class NotificationStackScrollLayout extends ViewGroup
mHideSensitiveNeedsAnimation = true;
mNeedsAnimation = true;
}
+ updateContentHeight();
requestChildrenUpdate();
}
}
@@ -3704,7 +3708,7 @@ public class NotificationStackScrollLayout extends ViewGroup
private void applyCurrentState() {
mCurrentStackScrollState.apply();
if (mListener != null) {
- mListener.onChildLocationsChanged(this);
+ mListener.onChildLocationsChanged();
}
runAnimationFinishedRunnables();
setAnimationRunning(false);
@@ -4188,6 +4192,26 @@ public class NotificationStackScrollLayout extends ViewGroup
}
}
+ @Override
+ public int getContainerChildCount() {
+ return getChildCount();
+ }
+
+ @Override
+ public View getContainerChildAt(int i) {
+ return getChildAt(i);
+ }
+
+ @Override
+ public void removeContainerView(View v) {
+ removeView(v);
+ }
+
+ @Override
+ public void addContainerView(View v) {
+ addView(v);
+ }
+
public void runAfterAnimationFinished(Runnable runnable) {
mAnimationFinishedRunnables.add(runnable);
}
@@ -4443,17 +4467,6 @@ public class NotificationStackScrollLayout extends ViewGroup
mAmbientState.getScrollY()));
}
- public void setTouchActive(boolean touchActive) {
- mShelf.setTouchActive(touchActive);
- }
-
- /**
- * A listener that is notified when some child locations might have changed.
- */
- public interface OnChildLocationsChangedListener {
- void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
- }
-
/**
* A listener that is notified when the empty space below the notifications is clicked on
*/
@@ -4709,6 +4722,7 @@ public class NotificationStackScrollLayout extends ViewGroup
}
}
+ @Override
public void resetExposedMenuView(boolean animate, boolean force) {
mSwipeHelper.resetExposedMenuView(animate, force);
}
diff --git a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index c060b088..7374f115 100644
--- a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -135,7 +135,7 @@ public class StackScrollAlgorithm {
for (int i = 0; i < childCount; i++) {
ExpandableView child = algorithmState.visibleChildren.get(i);
ExpandableViewState state = resultState.getViewStateForView(child);
- if (!child.mustStayOnScreen()) {
+ if (!child.mustStayOnScreen() || state.headsUpIsVisible) {
previousNotificationEnd = Math.max(drawStart, previousNotificationEnd);
previousNotificationStart = Math.max(drawStart, previousNotificationStart);
}
@@ -378,6 +378,13 @@ public class StackScrollAlgorithm {
boolean isEmptyShadeView = child instanceof EmptyShadeView;
childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
+ float inset = ambientState.getTopPadding() + ambientState.getStackTranslation();
+ if (child.mustStayOnScreen() && childViewState.yTranslation >= 0) {
+ // Even if we're not scrolled away we're in view and we're also not in the
+ // shelf. We can relax the constraints and let us scroll off the top!
+ float end = childViewState.yTranslation + childViewState.height + inset;
+ childViewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation();
+ }
if (isDismissView) {
childViewState.yTranslation = Math.min(childViewState.yTranslation,
ambientState.getInnerHeight() - childHeight);
@@ -396,8 +403,7 @@ public class StackScrollAlgorithm {
Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
}
- childViewState.yTranslation += ambientState.getTopPadding()
- + ambientState.getStackTranslation();
+ childViewState.yTranslation += inset;
return currentYPosition;
}
@@ -420,19 +426,21 @@ public class StackScrollAlgorithm {
break;
}
ExpandableViewState childState = resultState.getViewStateForView(row);
- if (topHeadsUpEntry == null) {
+ if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) {
topHeadsUpEntry = row;
childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
}
boolean isTopEntry = topHeadsUpEntry == row;
float unmodifiedEndLocation = childState.yTranslation + childState.height;
if (mIsExpanded) {
- // Ensure that the heads up is always visible even when scrolled off
- clampHunToTop(ambientState, row, childState);
- if (i == 0 && ambientState.isAboveShelf(row)) {
- // the first hun can't get off screen.
- clampHunToMaxTranslation(ambientState, row, childState);
- childState.hidden = false;
+ if (row.mustStayOnScreen() && !childState.headsUpIsVisible) {
+ // Ensure that the heads up is always visible even when scrolled off
+ clampHunToTop(ambientState, row, childState);
+ if (i == 0 && ambientState.isAboveShelf(row)) {
+ // the first hun can't get off screen.
+ clampHunToMaxTranslation(ambientState, row, childState);
+ childState.hidden = false;
+ }
}
}
if (row.isPinned()) {
@@ -440,7 +448,7 @@ public class StackScrollAlgorithm {
childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
childState.hidden = false;
ExpandableViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
- if (!isTopEntry && (!mIsExpanded
+ if (topState != null && !isTopEntry && (!mIsExpanded
|| unmodifiedEndLocation < topState.yTranslation + topState.height)) {
// Ensure that a headsUp doesn't vertically extend further than the heads-up at
// the top most z-position
@@ -493,6 +501,7 @@ public class StackScrollAlgorithm {
if (childViewState.yTranslation >= shelfStart) {
childViewState.hidden = true;
childViewState.inShelf = true;
+ childViewState.headsUpIsVisible = false;
}
if (!ambientState.isShadeExpanded()) {
childViewState.height = (int) (mStatusBarHeight - childViewState.yTranslation);
@@ -531,7 +540,8 @@ public class StackScrollAlgorithm {
ExpandableViewState childViewState = resultState.getViewStateForView(child);
int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements();
float baseZ = ambientState.getBaseZHeight();
- if (child.mustStayOnScreen() && !ambientState.isDozingAndNotPulsing(child)
+ if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible
+ && !ambientState.isDozingAndNotPulsing(child)
&& childViewState.yTranslation < ambientState.getTopPadding()
+ ambientState.getStackTranslation()) {
if (childrenOnTop != 0.0f) {
diff --git a/com/android/systemui/statusbar/stack/StackScrollState.java b/com/android/systemui/statusbar/stack/StackScrollState.java
index e3c746bf..588b7588 100644
--- a/com/android/systemui/statusbar/stack/StackScrollState.java
+++ b/com/android/systemui/statusbar/stack/StackScrollState.java
@@ -84,6 +84,7 @@ public class StackScrollState {
viewState.scaleX = view.getScaleX();
viewState.scaleY = view.getScaleY();
viewState.inShelf = false;
+ viewState.headsUpIsVisible = false;
}
public ExpandableViewState getViewStateForView(View requestedView) {
diff --git a/com/android/systemui/statusbar/stack/StackStateAnimator.java b/com/android/systemui/statusbar/stack/StackStateAnimator.java
index f78a718e..236c348e 100644
--- a/com/android/systemui/statusbar/stack/StackStateAnimator.java
+++ b/com/android/systemui/statusbar/stack/StackStateAnimator.java
@@ -40,7 +40,7 @@ import java.util.Stack;
public class StackStateAnimator {
public static final int ANIMATION_DURATION_STANDARD = 360;
- public static final int ANIMATION_DURATION_WAKEUP = 200;
+ public static final int ANIMATION_DURATION_WAKEUP = 500;
public static final int ANIMATION_DURATION_GO_TO_FULL_SHADE = 448;
public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
diff --git a/com/android/systemui/volume/Events.java b/com/android/systemui/volume/Events.java
index 49a12f40..e1376cad 100644
--- a/com/android/systemui/volume/Events.java
+++ b/com/android/systemui/volume/Events.java
@@ -80,6 +80,7 @@ public class Events {
public static final int DISMISS_REASON_SETTINGS_CLICKED = 5;
public static final int DISMISS_REASON_DONE_CLICKED = 6;
public static final int DISMISS_STREAM_GONE = 7;
+ public static final int DISMISS_REASON_OUTPUT_CHOOSER = 8;
public static final String[] DISMISS_REASONS = {
"unknown",
"touch_outside",
@@ -88,7 +89,8 @@ public class Events {
"screen_off",
"settings_clicked",
"done_clicked",
- "a11y_stream_changed"
+ "a11y_stream_changed",
+ "output_chooser"
};
public static final int SHOW_REASON_UNKNOWN = 0;
diff --git a/com/android/systemui/volume/OutputChooserDialog.java b/com/android/systemui/volume/OutputChooserDialog.java
new file mode 100644
index 00000000..f8843a99
--- /dev/null
+++ b/com/android/systemui/volume/OutputChooserDialog.java
@@ -0,0 +1,401 @@
+/*
+ * 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 com.android.systemui.volume;
+
+import static android.support.v7.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTED;
+import static android.support.v7.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTING;
+import static android.support.v7.media.MediaRouter.UNSELECT_REASON_DISCONNECTED;
+
+import static com.android.settingslib.bluetooth.Utils.getBtClassDrawableWithDescription;
+
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.v7.media.MediaControlIntent;
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.settingslib.Utils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.statusbar.policy.BluetoothController;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class OutputChooserDialog extends SystemUIDialog
+ implements DialogInterface.OnDismissListener, OutputChooserLayout.Callback {
+
+ private static final String TAG = Util.logTag(OutputChooserDialog.class);
+ private static final int MAX_DEVICES = 10;
+
+ private static final long UPDATE_DELAY_MS = 300L;
+ static final int MSG_UPDATE_ITEMS = 1;
+
+ private final Context mContext;
+ private final BluetoothController mController;
+ private final WifiManager mWifiManager;
+ private OutputChooserLayout mView;
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mRouterCallback;
+ private long mLastUpdateTime;
+
+ private final MediaRouteSelector mRouteSelector;
+ private Drawable mDefaultIcon;
+ private Drawable mTvIcon;
+ private Drawable mSpeakerIcon;
+ private Drawable mSpeakerGroupIcon;
+
+ public OutputChooserDialog(Context context) {
+ super(context);
+ mContext = context;
+ mController = Dependency.get(BluetoothController.class);
+ mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ mRouter = MediaRouter.getInstance(context);
+ mRouterCallback = new MediaRouterCallback();
+ mRouteSelector = new MediaRouteSelector.Builder()
+ .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ .build();
+
+ final IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ context.registerReceiver(mReceiver, filter);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.output_chooser);
+ setCanceledOnTouchOutside(true);
+ setOnDismissListener(this::onDismiss);
+ setTitle(R.string.output_title);
+
+ mView = findViewById(R.id.output_chooser);
+ mView.setCallback(this);
+
+ mDefaultIcon = mContext.getDrawable(R.drawable.ic_cast);
+ mTvIcon = mContext.getDrawable(R.drawable.ic_tv);
+ mSpeakerIcon = mContext.getDrawable(R.drawable.ic_speaker);
+ mSpeakerGroupIcon = mContext.getDrawable(R.drawable.ic_speaker_group);
+
+ final boolean wifiOff = !mWifiManager.isWifiEnabled();
+ final boolean btOff = !mController.isBluetoothEnabled();
+ if (wifiOff || btOff) {
+ mView.setEmptyState(getDisabledServicesMessage(wifiOff, btOff));
+ }
+ }
+
+ protected void cleanUp() {}
+
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mRouter.addCallback(mRouteSelector, mRouterCallback,
+ MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ mController.addCallback(mCallback);
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mRouter.removeCallback(mRouterCallback);
+ mController.removeCallback(mCallback);
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface unused) {
+ mContext.unregisterReceiver(mReceiver);
+ cleanUp();
+ }
+
+ @Override
+ public void onDetailItemClick(OutputChooserLayout.Item item) {
+ if (item == null || item.tag == null) return;
+ if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) {
+ final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag;
+ if (device != null && device.getMaxConnectionState()
+ == BluetoothProfile.STATE_DISCONNECTED) {
+ mController.connect(device);
+ }
+ } else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) {
+ final MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.tag;
+ if (route.isEnabled()) {
+ route.select();
+ }
+ }
+ }
+
+ @Override
+ public void onDetailItemDisconnect(OutputChooserLayout.Item item) {
+ if (item == null || item.tag == null) return;
+ if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) {
+ final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag;
+ if (device != null) {
+ mController.disconnect(device);
+ }
+ } else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) {
+ mRouter.unselect(UNSELECT_REASON_DISCONNECTED);
+ }
+ }
+
+ private void updateItems() {
+ if (SystemClock.uptimeMillis() - mLastUpdateTime < UPDATE_DELAY_MS) {
+ mHandler.removeMessages(MSG_UPDATE_ITEMS);
+ mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ITEMS),
+ mLastUpdateTime + UPDATE_DELAY_MS);
+ return;
+ }
+ mLastUpdateTime = SystemClock.uptimeMillis();
+ if (mView == null) return;
+ ArrayList<OutputChooserLayout.Item> items = new ArrayList<>();
+
+ // Add bluetooth devices
+ addBluetoothDevices(items);
+
+ // Add remote displays
+ addRemoteDisplayRoutes(items);
+
+ Collections.sort(items, ItemComparator.sInstance);
+
+ if (items.size() == 0) {
+ String emptyMessage = mContext.getString(R.string.output_none_found);
+ final boolean wifiOff = !mWifiManager.isWifiEnabled();
+ final boolean btOff = !mController.isBluetoothEnabled();
+ if (wifiOff || btOff) {
+ emptyMessage = getDisabledServicesMessage(wifiOff, btOff);
+ }
+ mView.setEmptyState(emptyMessage);
+ }
+
+ mView.setItems(items.toArray(new OutputChooserLayout.Item[items.size()]));
+ }
+
+ private String getDisabledServicesMessage(boolean wifiOff, boolean btOff) {
+ return mContext.getString(R.string.output_none_found_service_off,
+ wifiOff && btOff ? mContext.getString(R.string.output_service_bt_wifi)
+ : wifiOff ? mContext.getString(R.string.output_service_wifi)
+ : mContext.getString(R.string.output_service_bt));
+ }
+
+ private void addBluetoothDevices(List<OutputChooserLayout.Item> items) {
+ final Collection<CachedBluetoothDevice> devices = mController.getDevices();
+ if (devices != null) {
+ int connectedDevices = 0;
+ int count = 0;
+ for (CachedBluetoothDevice device : devices) {
+ if (mController.getBondState(device) == BluetoothDevice.BOND_NONE) continue;
+ final int majorClass = device.getBtClass().getMajorDeviceClass();
+ if (majorClass != BluetoothClass.Device.Major.AUDIO_VIDEO
+ && majorClass != BluetoothClass.Device.Major.UNCATEGORIZED) {
+ continue;
+ }
+ final OutputChooserLayout.Item item = new OutputChooserLayout.Item();
+ item.iconResId = R.drawable.ic_qs_bluetooth_on;
+ item.line1 = device.getName();
+ item.tag = device;
+ item.deviceType = OutputChooserLayout.Item.DEVICE_TYPE_BT;
+ int state = device.getMaxConnectionState();
+ if (state == BluetoothProfile.STATE_CONNECTED) {
+ item.iconResId = R.drawable.ic_qs_bluetooth_connected;
+ int batteryLevel = device.getBatteryLevel();
+ if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
+ Pair<Drawable, String> pair =
+ getBtClassDrawableWithDescription(getContext(), device);
+ item.icon = pair.first;
+ item.line2 = mContext.getString(
+ R.string.quick_settings_connected_battery_level,
+ Utils.formatPercentage(batteryLevel));
+ } else {
+ item.line2 = mContext.getString(R.string.quick_settings_connected);
+ }
+ item.canDisconnect = true;
+ items.add(connectedDevices, item);
+ connectedDevices++;
+ } else if (state == BluetoothProfile.STATE_CONNECTING) {
+ item.iconResId = R.drawable.ic_qs_bluetooth_connecting;
+ item.line2 = mContext.getString(R.string.quick_settings_connecting);
+ items.add(connectedDevices, item);
+ } else {
+ items.add(item);
+ }
+ if (++count == MAX_DEVICES) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void addRemoteDisplayRoutes(List<OutputChooserLayout.Item> items) {
+ List<MediaRouter.RouteInfo> routes = mRouter.getRoutes();
+ for(MediaRouter.RouteInfo route : routes) {
+ if (route.isDefaultOrBluetooth() || !route.isEnabled()
+ || !route.matchesSelector(mRouteSelector)) {
+ continue;
+ }
+ final OutputChooserLayout.Item item = new OutputChooserLayout.Item();
+ item.icon = getIconDrawable(route);
+ item.line1 = route.getName();
+ item.tag = route;
+ item.deviceType = OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER;
+ if (route.getConnectionState() == CONNECTION_STATE_CONNECTING) {
+ mContext.getString(R.string.quick_settings_connecting);
+ } else {
+ item.line2 = route.getDescription();
+ }
+
+ if (route.getConnectionState() == CONNECTION_STATE_CONNECTED) {
+ item.canDisconnect = true;
+ }
+ items.add(item);
+ }
+ }
+
+ private Drawable getIconDrawable(MediaRouter.RouteInfo route) {
+ Uri iconUri = route.getIconUri();
+ if (iconUri != null) {
+ try {
+ InputStream is = getContext().getContentResolver().openInputStream(iconUri);
+ Drawable drawable = Drawable.createFromStream(is, null);
+ if (drawable != null) {
+ return drawable;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to load " + iconUri, e);
+ // Falls back.
+ }
+ }
+ return getDefaultIconDrawable(route);
+ }
+
+ private Drawable getDefaultIconDrawable(MediaRouter.RouteInfo route) {
+ // If the type of the receiver device is specified, use it.
+ switch (route.getDeviceType()) {
+ case MediaRouter.RouteInfo.DEVICE_TYPE_TV:
+ return mTvIcon;
+ case MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER:
+ return mSpeakerIcon;
+ }
+
+ // Otherwise, make the best guess based on other route information.
+ if (route instanceof MediaRouter.RouteGroup) {
+ // Only speakers can be grouped for now.
+ return mSpeakerGroupIcon;
+ }
+ return mDefaultIcon;
+ }
+
+ private final class MediaRouterCallback extends MediaRouter.Callback {
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ updateItems();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ updateItems();
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ updateItems();
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+ dismiss();
+ }
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+ if (D.BUG) Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS");
+ cancel();
+ cleanUp();
+ }
+ }
+ };
+
+ private final BluetoothController.Callback mCallback = new BluetoothController.Callback() {
+ @Override
+ public void onBluetoothStateChange(boolean enabled) {
+ updateItems();
+ }
+
+ @Override
+ public void onBluetoothDevicesChanged() {
+ updateItems();
+ }
+ };
+
+ static final class ItemComparator implements Comparator<OutputChooserLayout.Item> {
+ public static final ItemComparator sInstance = new ItemComparator();
+
+ @Override
+ public int compare(OutputChooserLayout.Item lhs, OutputChooserLayout.Item rhs) {
+ // Connected item(s) first
+ if (lhs.canDisconnect != rhs.canDisconnect) {
+ return Boolean.compare(rhs.canDisconnect, lhs.canDisconnect);
+ }
+ // Bluetooth items before media routes
+ if (lhs.deviceType != rhs.deviceType) {
+ return Integer.compare(lhs.deviceType, rhs.deviceType);
+ }
+ // then by name
+ return lhs.line1.toString().compareToIgnoreCase(rhs.line1.toString());
+ }
+ }
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_UPDATE_ITEMS:
+ updateItems();
+ break;
+ }
+ }
+ };
+} \ No newline at end of file
diff --git a/com/android/systemui/volume/OutputChooserLayout.java b/com/android/systemui/volume/OutputChooserLayout.java
new file mode 100644
index 00000000..22ced600
--- /dev/null
+++ b/com/android/systemui/volume/OutputChooserLayout.java
@@ -0,0 +1,258 @@
+/*
+ * 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 com.android.systemui.volume;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.systemui.FontSizeUtils;
+import com.android.systemui.R;
+import com.android.systemui.qs.AutoSizingList;
+
+/**
+ * Limited height list of devices.
+ */
+public class OutputChooserLayout extends FrameLayout {
+ private static final String TAG = "OutputChooserLayout";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final int mQsDetailIconOverlaySize;
+ private final Context mContext;
+ private final H mHandler = new H();
+ private final Adapter mAdapter = new Adapter();
+
+ private String mTag;
+ private Callback mCallback;
+ private boolean mItemsVisible = true;
+ private AutoSizingList mItemList;
+ private View mEmpty;
+ private TextView mEmptyText;
+
+ private Item[] mItems;
+
+ public OutputChooserLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ mTag = TAG;
+ mQsDetailIconOverlaySize = (int) getResources().getDimension(
+ R.dimen.qs_detail_icon_overlay_size);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mItemList = findViewById(android.R.id.list);
+ mItemList.setVisibility(GONE);
+ mItemList.setAdapter(mAdapter);
+ mEmpty = findViewById(android.R.id.empty);
+ mEmpty.setVisibility(GONE);
+ mEmptyText = mEmpty.findViewById(android.R.id.title);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ FontSizeUtils.updateFontSize(mEmptyText, R.dimen.qs_detail_empty_text_size);
+ int count = mItemList.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View item = mItemList.getChildAt(i);
+ FontSizeUtils.updateFontSize(item, android.R.id.title,
+ R.dimen.qs_detail_item_primary_text_size);
+ FontSizeUtils.updateFontSize(item, android.R.id.summary,
+ R.dimen.qs_detail_item_secondary_text_size);
+ }
+ }
+
+ public void setEmptyState(String text) {
+ mEmpty.post(() -> {
+ mEmptyText.setText(text);
+ });
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (DEBUG) Log.d(mTag, "onAttachedToWindow");
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (DEBUG) Log.d(mTag, "onDetachedFromWindow");
+ mCallback = null;
+ }
+
+ public void setCallback(Callback callback) {
+ mHandler.removeMessages(H.SET_CALLBACK);
+ mHandler.obtainMessage(H.SET_CALLBACK, callback).sendToTarget();
+ }
+
+ public void setItems(Item[] items) {
+ mHandler.removeMessages(H.SET_ITEMS);
+ mHandler.obtainMessage(H.SET_ITEMS, items).sendToTarget();
+ }
+
+ public void setItemsVisible(boolean visible) {
+ mHandler.removeMessages(H.SET_ITEMS_VISIBLE);
+ mHandler.obtainMessage(H.SET_ITEMS_VISIBLE, visible ? 1 : 0, 0).sendToTarget();
+ }
+
+ private void handleSetCallback(Callback callback) {
+ mCallback = callback;
+ }
+
+ private void handleSetItems(Item[] items) {
+ final int itemCount = items != null ? items.length : 0;
+ mEmpty.setVisibility(itemCount == 0 ? VISIBLE : GONE);
+ mItemList.setVisibility(itemCount == 0 ? GONE : VISIBLE);
+ mItems = items;
+ mAdapter.notifyDataSetChanged();
+ }
+
+ private void handleSetItemsVisible(boolean visible) {
+ if (mItemsVisible == visible) return;
+ mItemsVisible = visible;
+ for (int i = 0; i < mItemList.getChildCount(); i++) {
+ mItemList.getChildAt(i).setVisibility(mItemsVisible ? VISIBLE : INVISIBLE);
+ }
+ }
+
+ private class Adapter extends BaseAdapter {
+
+ @Override
+ public int getCount() {
+ return mItems != null ? mItems.length : 0;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mItems[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ final Item item = mItems[position];
+ if (view == null) {
+ view = LayoutInflater.from(mContext).inflate(R.layout.output_chooser_item, parent,
+ false);
+ }
+ view.setVisibility(mItemsVisible ? VISIBLE : INVISIBLE);
+ final ImageView iv = view.findViewById(android.R.id.icon);
+ if (item.icon != null) {
+ iv.setImageDrawable(item.icon);
+ } else {
+ iv.setImageResource(item.iconResId);
+ }
+ iv.getOverlay().clear();
+ if (item.overlay != null) {
+ item.overlay.setBounds(0, 0, mQsDetailIconOverlaySize, mQsDetailIconOverlaySize);
+ iv.getOverlay().add(item.overlay);
+ }
+ final TextView title = view.findViewById(android.R.id.title);
+ title.setText(item.line1);
+ final TextView summary = view.findViewById(android.R.id.summary);
+ final boolean twoLines = !TextUtils.isEmpty(item.line2);
+ title.setMaxLines(twoLines ? 1 : 2);
+ summary.setVisibility(twoLines ? VISIBLE : GONE);
+ summary.setText(twoLines ? item.line2 : null);
+ view.setOnClickListener(v -> {
+ if (mCallback != null) {
+ mCallback.onDetailItemClick(item);
+ }
+ });
+
+ final ImageView icon2 = view.findViewById(android.R.id.icon2);
+ if (item.canDisconnect) {
+ icon2.setImageResource(R.drawable.ic_qs_cancel);
+ icon2.setVisibility(VISIBLE);
+ icon2.setClickable(true);
+ icon2.setOnClickListener(v -> {
+ if (mCallback != null) {
+ mCallback.onDetailItemDisconnect(item);
+ }
+ });
+ } else if (item.icon2 != -1) {
+ icon2.setVisibility(VISIBLE);
+ icon2.setImageResource(item.icon2);
+ icon2.setClickable(false);
+ } else {
+ icon2.setVisibility(GONE);
+ }
+
+ return view;
+ }
+ };
+
+ private class H extends Handler {
+ private static final int SET_ITEMS = 1;
+ private static final int SET_CALLBACK = 2;
+ private static final int SET_ITEMS_VISIBLE = 3;
+
+ public H() {
+ super(Looper.getMainLooper());
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == SET_ITEMS) {
+ handleSetItems((Item[]) msg.obj);
+ } else if (msg.what == SET_CALLBACK) {
+ handleSetCallback((OutputChooserLayout.Callback) msg.obj);
+ } else if (msg.what == SET_ITEMS_VISIBLE) {
+ handleSetItemsVisible(msg.arg1 != 0);
+ }
+ }
+ }
+
+ public static class Item {
+ public static int DEVICE_TYPE_BT = 1;
+ public static int DEVICE_TYPE_MEDIA_ROUTER = 2;
+ public int iconResId;
+ public Drawable icon;
+ public Drawable overlay;
+ public CharSequence line1;
+ public CharSequence line2;
+ public Object tag;
+ public boolean canDisconnect;
+ public int icon2 = -1;
+ public int deviceType = 0;
+ }
+
+ public interface Callback {
+ void onDetailItemClick(Item item);
+ void onDetailItemDisconnect(Item item);
+ }
+}
diff --git a/com/android/systemui/volume/VolumeDialogComponent.java b/com/android/systemui/volume/VolumeDialogComponent.java
index 4dff9bd5..bc98140f 100644
--- a/com/android/systemui/volume/VolumeDialogComponent.java
+++ b/com/android/systemui/volume/VolumeDialogComponent.java
@@ -134,6 +134,10 @@ public class VolumeDialogComponent implements VolumeComponent, TunerService.Tuna
mController.setVolumePolicy(mVolumePolicy);
}
+ void setEnableDialogs(boolean volumeUi, boolean safetyWarning) {
+ mController.setEnableDialogs(volumeUi, safetyWarning);
+ }
+
@Override
public void onUserActivity() {
final KeyguardViewMediator kvm = mSysui.getComponent(KeyguardViewMediator.class);
diff --git a/com/android/systemui/volume/VolumeDialogControllerImpl.java b/com/android/systemui/volume/VolumeDialogControllerImpl.java
index b08b26d7..4464f75b 100644
--- a/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -49,7 +49,6 @@ import com.android.internal.annotations.GuardedBy;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
-import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.plugins.VolumeDialogController;
import com.android.systemui.qs.tiles.DndTile;
@@ -104,6 +103,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
private final Vibrator mVibrator;
private final boolean mHasVibrator;
private boolean mShowA11yStream;
+ private boolean mShowVolumeDialog;
+ private boolean mShowSafetyWarning;
private boolean mDestroyed;
private VolumePolicy mVolumePolicy;
@@ -167,7 +168,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
}
protected int getAudioManagerStreamMinVolume(int stream) {
- return mAudio.getStreamMinVolume(stream);
+ return mAudio.getStreamMinVolumeInt(stream);
}
public void register() {
@@ -283,6 +284,11 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
mWorker.obtainMessage(W.SET_ACTIVE_STREAM, stream, 0).sendToTarget();
}
+ public void setEnableDialogs(boolean volumeUi, boolean safetyWarning) {
+ mShowVolumeDialog = volumeUi;
+ mShowSafetyWarning = safetyWarning;
+ }
+
public void vibrate() {
if (mHasVibrator) {
mVibrator.vibrate(VIBRATE_HINT_DURATION);
@@ -312,7 +318,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
}
private void onShowSafetyWarningW(int flags) {
- mCallbacks.onShowSafetyWarning(flags);
+ if (mShowSafetyWarning) {
+ mCallbacks.onShowSafetyWarning(flags);
+ }
}
private void onAccessibilityModeChanged(Boolean showA11yStream) {
@@ -344,7 +352,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
&& mStatusBar.getWakefulnessState() != WakefulnessLifecycle.WAKEFULNESS_ASLEEP
&& mStatusBar.getWakefulnessState() != WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP
&& mStatusBar.isDeviceInteractive()
- && (flags & AudioManager.FLAG_SHOW_UI) != 0;
+ && (flags & AudioManager.FLAG_SHOW_UI) != 0
+ && mShowVolumeDialog;
}
boolean onVolumeChangedW(int stream, int flags) {
diff --git a/com/android/systemui/volume/VolumeDialogImpl.java b/com/android/systemui/volume/VolumeDialogImpl.java
index 383d3276..d7c80101 100644
--- a/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/com/android/systemui/volume/VolumeDialogImpl.java
@@ -19,6 +19,7 @@ package com.android.systemui.volume;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
+import static com.android.systemui.volume.Events.DISMISS_REASON_OUTPUT_CHOOSER;
import static com.android.systemui.volume.Events.DISMISS_REASON_TOUCH_OUTSIDE;
import android.accessibilityservice.AccessibilityServiceInfo;
@@ -29,12 +30,14 @@ import android.app.Dialog;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
-import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Rect;
+import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.os.Debug;
@@ -43,6 +46,8 @@ import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.provider.Settings.Global;
+import android.transition.AutoTransition;
+import android.transition.TransitionManager;
import android.util.Log;
import android.util.Slog;
import android.util.SparseBooleanArray;
@@ -67,7 +72,6 @@ import android.widget.TextView;
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
-import com.android.systemui.HardwareUiLayout;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.plugins.VolumeDialog;
@@ -97,22 +101,27 @@ public class VolumeDialogImpl implements VolumeDialog {
private final VolumeDialogController mController;
private Window mWindow;
- private HardwareUiLayout mHardwareLayout;
private CustomDialog mDialog;
private ViewGroup mDialogView;
private ViewGroup mDialogRowsView;
- private ViewGroup mDialogContentView;
+ private ImageButton mExpandButton;
+ private ImageButton mRingerIcon;
+ private ImageButton mOutputChooser;
+ private TextView mRingerStatus;
private final List<VolumeRow> mRows = new ArrayList<>();
private ConfigurableTexts mConfigurableTexts;
private final SparseBooleanArray mDynamic = new SparseBooleanArray();
private final KeyguardManager mKeyguard;
private final AccessibilityManager mAccessibilityMgr;
private final Object mSafetyWarningLock = new Object();
+ private final Object mOutputChooserLock = new Object();
private final Accessibility mAccessibility = new Accessibility();
private final ColorStateList mActiveSliderTint;
private final ColorStateList mInactiveSliderTint;
private boolean mShowing;
+ private boolean mExpanded;
+ private boolean mExpandButtonAnimationRunning;
private boolean mShowA11yStream;
private int mActiveStream;
@@ -121,6 +130,7 @@ public class VolumeDialogImpl implements VolumeDialog {
private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE;
private State mState;
private SafetyWarningDialog mSafetyWarning;
+ private OutputChooserDialog mOutputChooserDialog;
private boolean mHovering = false;
public VolumeDialogImpl(Context context) {
@@ -160,54 +170,77 @@ public class VolumeDialogImpl implements VolumeDialog {
mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
- mWindow.addFlags(
- WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
- | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
- | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
- | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
+ mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
+ mWindow.setTitle(VolumeDialogImpl.class.getSimpleName());
mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY);
mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast);
mDialog.setContentView(R.layout.volume_dialog);
- mDialogView = (ViewGroup) mDialog.findViewById(R.id.volume_dialog);
- mDialogView.setOnHoverListener(new View.OnHoverListener() {
- @Override
- public boolean onHover(View v, MotionEvent event) {
- int action = event.getActionMasked();
- mHovering = (action == MotionEvent.ACTION_HOVER_ENTER)
- || (action == MotionEvent.ACTION_HOVER_MOVE);
- rescheduleTimeoutH();
- return true;
- }
+ mDialog.setOnShowListener(dialog -> {
+ mDialogView.setTranslationY(-mDialogView.getHeight());
+ mDialogView.setAlpha(0);
+ mDialogView.animate()
+ .alpha(1)
+ .translationY(0)
+ .setDuration(300)
+ .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
+ .withEndAction(() -> {
+ mWindow.getDecorView().requestAccessibilityFocus();
+ })
+ .start();
+ });
+ mDialogView = mDialog.findViewById(R.id.volume_dialog);
+ mDialogView.setOnHoverListener((v, event) -> {
+ int action = event.getActionMasked();
+ mHovering = (action == MotionEvent.ACTION_HOVER_ENTER)
+ || (action == MotionEvent.ACTION_HOVER_MOVE);
+ rescheduleTimeoutH();
+ return true;
});
- mHardwareLayout = HardwareUiLayout.get(mDialogView);
- mHardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE));
+ VolumeUiLayout hardwareLayout = VolumeUiLayout.get(mDialogView);
+ hardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE));
- mDialogContentView = mDialog.findViewById(R.id.volume_dialog_content);
- mDialogRowsView = mDialogContentView.findViewById(R.id.volume_dialog_rows);
+ ViewGroup dialogContentView = mDialog.findViewById(R.id.volume_dialog_content);
+ mDialogRowsView = dialogContentView.findViewById(R.id.volume_dialog_rows);
+ mRingerIcon = mDialog.findViewById(R.id.ringer_icon);
+ mRingerStatus = mDialog.findViewById(R.id.ringer_status);
+
+ mExpanded = false;
+ mExpandButton = mDialogView.findViewById(R.id.volume_expand_button);
+ mExpandButton.setOnClickListener(mClickExpand);
+ mExpandButton.setVisibility(
+ AudioSystem.isSingleVolume(mContext) ? View.GONE : View.VISIBLE);
+
+ mOutputChooser = mDialogView.findViewById(R.id.output_chooser);
+ mOutputChooser.setOnClickListener(mClickOutputChooser);
if (mRows.isEmpty()) {
addRow(AudioManager.STREAM_MUSIC,
- R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true);
+ R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true, true);
if (!AudioSystem.isSingleVolume(mContext)) {
addRow(AudioManager.STREAM_RING,
- R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true);
+ R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true, false);
addRow(AudioManager.STREAM_ALARM,
- R.drawable.ic_volume_alarm, R.drawable.ic_volume_alarm_mute, false);
+ R.drawable.ic_volume_alarm, R.drawable.ic_volume_alarm_mute, true, false);
addRow(AudioManager.STREAM_VOICE_CALL,
- R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, false);
+ R.drawable.ic_volume_voice, R.drawable.ic_volume_voice, false, false);
addRow(AudioManager.STREAM_BLUETOOTH_SCO,
- R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false);
- addRow(AudioManager.STREAM_SYSTEM,
- R.drawable.ic_volume_system, R.drawable.ic_volume_system_mute, false);
+ R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false, false);
+ addRow(AudioManager.STREAM_SYSTEM, R.drawable.ic_volume_system,
+ R.drawable.ic_volume_system_mute, false, false);
addRow(AudioManager.STREAM_ACCESSIBILITY, R.drawable.ic_volume_accessibility,
- R.drawable.ic_volume_accessibility, true);
+ R.drawable.ic_volume_accessibility, true, false);
}
} else {
addExistingRows();
}
updateRowsH(getActiveRow());
+ initRingerH();
}
private ColorStateList loadColorStateList(int colorResId) {
@@ -230,14 +263,15 @@ public class VolumeDialogImpl implements VolumeDialog {
mHandler.sendEmptyMessage(H.RECHECK_ALL);
}
- private void addRow(int stream, int iconRes, int iconMuteRes, boolean important) {
- addRow(stream, iconRes, iconMuteRes, important, false);
+ private void addRow(int stream, int iconRes, int iconMuteRes, boolean important,
+ boolean defaultStream) {
+ addRow(stream, iconRes, iconMuteRes, important, defaultStream, false);
}
private void addRow(int stream, int iconRes, int iconMuteRes, boolean important,
- boolean dynamic) {
+ boolean defaultStream, boolean dynamic) {
VolumeRow row = new VolumeRow();
- initRow(row, stream, iconRes, iconMuteRes, important);
+ initRow(row, stream, iconRes, iconMuteRes, important, defaultStream);
int rowSize;
int viewSize;
if (mShowA11yStream && dynamic && (rowSize = mRows.size()) > 1
@@ -255,7 +289,8 @@ public class VolumeDialogImpl implements VolumeDialog {
int N = mRows.size();
for (int i = 0; i < N; i++) {
final VolumeRow row = mRows.get(i);
- initRow(row, row.stream, row.iconRes, row.iconMuteRes, row.important);
+ initRow(row, row.stream, row.iconRes, row.iconMuteRes, row.important,
+ row.defaultStream);
mDialogRowsView.addView(row.view);
updateVolumeRowH(row);
}
@@ -280,6 +315,7 @@ public class VolumeDialogImpl implements VolumeDialog {
public void dump(PrintWriter writer) {
writer.println(VolumeDialogImpl.class.getSimpleName() + " state:");
writer.print(" mShowing: "); writer.println(mShowing);
+ writer.print(" mExpanded: "); writer.println(mExpanded);
writer.print(" mActiveStream: "); writer.println(mActiveStream);
writer.print(" mDynamic: "); writer.println(mDynamic);
writer.print(" mAutomute: "); writer.println(mAutomute);
@@ -298,11 +334,12 @@ public class VolumeDialogImpl implements VolumeDialog {
@SuppressLint("InflateParams")
private void initRow(final VolumeRow row, final int stream, int iconRes, int iconMuteRes,
- boolean important) {
+ boolean important, boolean defaultStream) {
row.stream = stream;
row.iconRes = iconRes;
row.iconMuteRes = iconMuteRes;
row.important = important;
+ row.defaultStream = defaultStream;
row.view = mDialog.getLayoutInflater().inflate(R.layout.volume_dialog_row, null);
row.view.setId(row.stream);
row.view.setTag(row);
@@ -340,40 +377,61 @@ public class VolumeDialogImpl implements VolumeDialog {
row.icon = row.view.findViewById(R.id.volume_row_icon);
row.icon.setImageResource(iconRes);
if (row.stream != AudioSystem.STREAM_ACCESSIBILITY) {
- row.icon.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, row.stream, row.iconState);
- mController.setActiveStream(row.stream);
- if (row.stream == AudioManager.STREAM_RING) {
- final boolean hasVibrator = mController.hasVibrator();
- if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) {
- if (hasVibrator) {
- mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false);
- } else {
- final boolean wasZero = row.ss.level == 0;
- mController.setStreamVolume(stream,
- wasZero ? row.lastAudibleLevel : 0);
- }
+ row.icon.setOnClickListener(v -> {
+ Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, row.stream, row.iconState);
+ mController.setActiveStream(row.stream);
+ if (row.stream == AudioManager.STREAM_RING) {
+ final boolean hasVibrator = mController.hasVibrator();
+ if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) {
+ if (hasVibrator) {
+ mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false);
} else {
- mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false);
- if (row.ss.level == 0) {
- mController.setStreamVolume(stream, 1);
- }
+ final boolean wasZero = row.ss.level == 0;
+ mController.setStreamVolume(stream,
+ wasZero ? row.lastAudibleLevel : 0);
}
} else {
- final boolean vmute = row.ss.level == row.ss.levelMin;
- mController.setStreamVolume(stream,
- vmute ? row.lastAudibleLevel : row.ss.levelMin);
+ mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false);
+ if (row.ss.level == 0) {
+ mController.setStreamVolume(stream, 1);
+ }
}
- row.userAttempt = 0; // reset the grace period, slider updates immediately
+ } else {
+ final boolean vmute = row.ss.level == row.ss.levelMin;
+ mController.setStreamVolume(stream,
+ vmute ? row.lastAudibleLevel : row.ss.levelMin);
}
+ row.userAttempt = 0; // reset the grace period, slider updates immediately
});
} else {
row.icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
}
+ public void initRingerH() {
+ mRingerIcon.setOnClickListener(v -> {
+ Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, AudioManager.STREAM_RING,
+ mRingerIcon.getTag());
+ final StreamState ss = mState.states.get(AudioManager.STREAM_RING);
+ final boolean hasVibrator = mController.hasVibrator();
+ if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) {
+ if (hasVibrator) {
+ mController.setRingerMode(AudioManager.RINGER_MODE_VIBRATE, false);
+ } else {
+ final boolean wasZero = ss.level == 0;
+ mController.setStreamVolume(AudioManager.STREAM_RING, wasZero ? 1 : 0);
+ }
+ } else {
+ mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false);
+ if (ss.level == 0) {
+ mController.setStreamVolume(AudioManager.STREAM_RING, 1);
+ }
+ }
+ updateRingerH();
+ });
+ updateRingerH();
+ }
+
public void show(int reason) {
mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget();
}
@@ -389,27 +447,12 @@ public class VolumeDialogImpl implements VolumeDialog {
rescheduleTimeoutH();
if (mShowing) return;
mShowing = true;
- mHardwareLayout.setTranslationX(getAnimTranslation());
- mHardwareLayout.setAlpha(0);
- mHardwareLayout.animate()
- .alpha(1)
- .translationX(0)
- .setDuration(300)
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
- .withEndAction(() -> {
- mDialog.show();
- mWindow.getDecorView().requestAccessibilityFocus();
- })
- .start();
+
+ mDialog.show();
Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
mController.notifyVisible(true);
}
- private float getAnimTranslation() {
- return mContext.getResources().getDimension(
- R.dimen.volume_dialog_panel_width) / 2;
- }
-
protected void rescheduleTimeoutH() {
mHandler.removeMessages(H.DISMISS);
final int timeout = computeTimeoutH();
@@ -422,8 +465,8 @@ public class VolumeDialogImpl implements VolumeDialog {
private int computeTimeoutH() {
if (mAccessibility.mFeedbackEnabled) return 20000;
if (mHovering) return 16000;
+ if (mExpanded) return 5000;
if (mSafetyWarning != null) return 5000;
- if (mActiveStream == AudioManager.STREAM_MUSIC) return 1500;
return 3000;
}
@@ -431,18 +474,25 @@ public class VolumeDialogImpl implements VolumeDialog {
mHandler.removeMessages(H.DISMISS);
mHandler.removeMessages(H.SHOW);
if (!mShowing) return;
+ mDialogView.animate().cancel();
mShowing = false;
- mHardwareLayout.setTranslationX(0);
- mHardwareLayout.setAlpha(1);
- mHardwareLayout.animate()
+
+ updateExpandedH(false /* expanding */, true /* dismissing */);
+
+ mDialogView.setTranslationY(0);
+ mDialogView.setAlpha(1);
+ mDialogView.animate()
.alpha(0)
- .translationX(getAnimTranslation())
- .setDuration(300)
- .withEndAction(() -> mDialog.dismiss())
+ .translationY(-mDialogView.getHeight())
+ .setDuration(250)
.setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
+ .withEndAction(() -> mHandler.postDelayed(() -> {
+ if (D.BUG) Log.d(TAG, "mDialog.dismiss()");
+ mDialog.dismiss();
+ }, 50))
.start();
- if (mAccessibilityMgr.isObservedEventType(
- AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)) {
+
+ if (mAccessibilityMgr.isEnabled()) {
AccessibilityEvent event =
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
event.setPackageName(mContext.getPackageName());
@@ -461,6 +511,76 @@ public class VolumeDialogImpl implements VolumeDialog {
}
}
+ private void updateExpandedH(final boolean expanded, final boolean dismissing) {
+ if (D.BUG) Log.d(TAG, "updateExpandedH " + expanded);
+
+ if (mExpanded == expanded) return;
+ mExpanded = expanded;
+ mExpandButtonAnimationRunning = isAttached();
+ updateExpandButtonH();
+ TransitionManager.endTransitions(mDialogView);
+ final VolumeRow activeRow = getActiveRow();
+ if (!dismissing) {
+ mWindow.setLayout(mWindow.getAttributes().width, ViewGroup.LayoutParams.MATCH_PARENT);
+ TransitionManager.beginDelayedTransition(mDialogView, getTransition());
+ }
+ updateRowsH(activeRow);
+ rescheduleTimeoutH();
+ }
+
+ private AutoTransition getTransition() {
+ AutoTransition transition = new AutoTransition();
+ transition.setDuration(300);
+ transition.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ return transition;
+ }
+
+ private void updateExpandButtonH() {
+ if (D.BUG) Log.d(TAG, "updateExpandButtonH");
+
+ mExpandButton.setClickable(!mExpandButtonAnimationRunning);
+ if (!(mExpandButtonAnimationRunning && isAttached())) {
+ final int res = mExpanded ? R.drawable.ic_volume_collapse_animation
+ : R.drawable.ic_volume_expand_animation;
+ if (hasTouchFeature()) {
+ mExpandButton.setImageResource(res);
+ } else {
+ // if there is no touch feature, show the volume ringer instead
+ mExpandButton.setImageResource(R.drawable.ic_volume_ringer);
+ mExpandButton.setBackgroundResource(0); // remove gray background emphasis
+ }
+ mExpandButton.setContentDescription(mContext.getString(mExpanded ?
+ R.string.accessibility_volume_collapse : R.string.accessibility_volume_expand));
+ }
+ if (mExpandButtonAnimationRunning) {
+ final Drawable d = mExpandButton.getDrawable();
+ if (d instanceof AnimatedVectorDrawable) {
+ // workaround to reset drawable
+ final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) d.getConstantState()
+ .newDrawable();
+ mExpandButton.setImageDrawable(avd);
+ avd.start();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mExpandButtonAnimationRunning = false;
+ updateExpandButtonH();
+ rescheduleTimeoutH();
+ }
+ }, 300);
+ }
+ }
+ }
+
+ private boolean isAttached() {
+ return mDialogView != null && mDialogView.isAttachedToWindow();
+ }
+
+ private boolean hasTouchFeature() {
+ final PackageManager pm = mContext.getPackageManager();
+ return pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);
+ }
+
private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) {
boolean isActive = row == activeRow;
if (row.stream == AudioSystem.STREAM_ACCESSIBILITY) {
@@ -474,7 +594,7 @@ public class VolumeDialogImpl implements VolumeDialog {
return true;
}
- return row.important || isActive;
+ return row.defaultStream || isActive || (mExpanded && row.important);
}
private void updateRowsH(final VolumeRow activeRow) {
@@ -494,6 +614,53 @@ public class VolumeDialogImpl implements VolumeDialog {
}
}
+ protected void updateRingerH() {
+ if (mState != null) {
+ final StreamState ss = mState.states.get(AudioManager.STREAM_RING);
+ switch (mState.ringerModeInternal) {
+ case AudioManager.RINGER_MODE_VIBRATE:
+ mRingerStatus.setText(R.string.volume_ringer_status_vibrate);
+ mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate);
+ mRingerIcon.setTag(Events.ICON_STATE_VIBRATE);
+ break;
+ case AudioManager.RINGER_MODE_SILENT:
+ mRingerStatus.setText(R.string.volume_ringer_status_silent);
+ mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
+ mRingerIcon.setContentDescription(mContext.getString(
+ R.string.volume_stream_content_description_unmute,
+ getStreamLabelH(ss)));
+ mRingerIcon.setTag(Events.ICON_STATE_MUTE);
+ break;
+ case AudioManager.RINGER_MODE_NORMAL:
+ default:
+ boolean muted = (mAutomute && ss.level == 0) || ss.muted;
+ if (muted) {
+ mRingerStatus.setText(R.string.volume_ringer_status_silent);
+ mRingerIcon.setImageResource(R.drawable.ic_volume_ringer_mute);
+ mRingerIcon.setContentDescription(mContext.getString(
+ R.string.volume_stream_content_description_unmute,
+ getStreamLabelH(ss)));
+ mRingerIcon.setTag(Events.ICON_STATE_MUTE);
+ } else {
+ mRingerStatus.setText(R.string.volume_ringer_status_normal);
+ mRingerIcon.setImageResource(R.drawable.ic_volume_ringer);
+ if (mController.hasVibrator()) {
+ mRingerIcon.setContentDescription(mContext.getString(
+ mShowA11yStream
+ ? R.string.volume_stream_content_description_vibrate_a11y
+ : R.string.volume_stream_content_description_vibrate,
+ getStreamLabelH(ss)));
+
+ } else {
+ mRingerIcon.setContentDescription(getStreamLabelH(ss));
+ }
+ mRingerIcon.setTag(Events.ICON_STATE_UNMUTE);
+ }
+ break;
+ }
+ }
+ }
+
private void trimObsoleteH() {
if (D.BUG) Log.d(TAG, "trimObsoleteH");
for (int i = mRows.size() - 1; i >= 0; i--) {
@@ -517,7 +684,7 @@ public class VolumeDialogImpl implements VolumeDialog {
mDynamic.put(stream, true);
if (findRow(stream) == null) {
addRow(stream, R.drawable.ic_volume_remote, R.drawable.ic_volume_remote_mute, true,
- true);
+ false, true);
}
}
@@ -530,6 +697,7 @@ public class VolumeDialogImpl implements VolumeDialog {
for (VolumeRow row : mRows) {
updateVolumeRowH(row);
}
+ updateRingerH();
}
private void updateVolumeRowH(VolumeRow row) {
@@ -751,6 +919,23 @@ public class VolumeDialogImpl implements VolumeDialog {
rescheduleTimeoutH();
}
+ private void showOutputChooserH() {
+ synchronized (mOutputChooserLock) {
+ if (mOutputChooserDialog != null) {
+ return;
+ }
+ mOutputChooserDialog = new OutputChooserDialog(mContext) {
+ @Override
+ protected void cleanUp() {
+ synchronized (mOutputChooserLock) {
+ mOutputChooserDialog = null;
+ }
+ }
+ };
+ mOutputChooserDialog.show();
+ }
+ }
+
private String getStreamLabelH(StreamState ss) {
if (ss.remoteLabel != null) {
return ss.remoteLabel;
@@ -763,6 +948,25 @@ public class VolumeDialogImpl implements VolumeDialog {
}
}
+ private final OnClickListener mClickExpand = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mExpandButton.animate().cancel();
+ final boolean newExpand = !mExpanded;
+ Events.writeEvent(mContext, Events.EVENT_EXPAND, newExpand);
+ updateExpandedH(newExpand, false /* dismissing */);
+ }
+ };
+
+ private final OnClickListener mClickOutputChooser = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // TODO: log
+ dismissH(DISMISS_REASON_OUTPUT_CHOOSER);
+ showOutputChooserH();
+ }
+ };
+
private final VolumeDialogController.Callbacks mControllerCallbackH
= new VolumeDialogController.Callbacks() {
@Override
@@ -818,8 +1022,7 @@ public class VolumeDialogImpl implements VolumeDialog {
@Override
public void onAccessibilityModeChanged(Boolean showA11yStream) {
- boolean show = showA11yStream == null ? false : showA11yStream;
- mShowA11yStream = show;
+ mShowA11yStream = showA11yStream == null ? false : showA11yStream;
VolumeRow activeRow = getActiveRow();
if (!mShowA11yStream && AudioManager.STREAM_ACCESSIBILITY == activeRow.stream) {
dismissH(Events.DISMISS_STREAM_GONE);
@@ -1020,6 +1223,7 @@ public class VolumeDialogImpl implements VolumeDialog {
private int iconRes;
private int iconMuteRes;
private boolean important;
+ private boolean defaultStream;
private ColorStateList cachedSliderTint;
private int iconState; // from Events
private ObjectAnimator anim; // slider progress animation for non-touch-related updates
diff --git a/com/android/systemui/volume/VolumeUI.java b/com/android/systemui/volume/VolumeUI.java
index 02969e48..6f65b081 100644
--- a/com/android/systemui/volume/VolumeUI.java
+++ b/com/android/systemui/volume/VolumeUI.java
@@ -40,9 +40,13 @@ public class VolumeUI extends SystemUI {
@Override
public void start() {
- mEnabled = mContext.getResources().getBoolean(R.bool.enable_volume_ui);
+ boolean enableVolumeUi = mContext.getResources().getBoolean(R.bool.enable_volume_ui);
+ boolean enableSafetyWarning =
+ mContext.getResources().getBoolean(R.bool.enable_safety_warning);
+ mEnabled = enableVolumeUi || enableSafetyWarning;
if (!mEnabled) return;
mVolumeComponent = new VolumeDialogComponent(this, mContext, null);
+ mVolumeComponent.setEnableDialogs(enableVolumeUi, enableSafetyWarning);
putComponent(VolumeComponent.class, getVolumeComponent());
setDefaultVolumeController();
}
diff --git a/com/android/systemui/volume/VolumeUiLayout.java b/com/android/systemui/volume/VolumeUiLayout.java
new file mode 100644
index 00000000..49ac9b6b
--- /dev/null
+++ b/com/android/systemui/volume/VolumeUiLayout.java
@@ -0,0 +1,66 @@
+/*
+ * 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 com.android.systemui.volume;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+
+public class VolumeUiLayout extends FrameLayout {
+
+ public VolumeUiLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnComputeInternalInsetsListener(mInsetsListener);
+ }
+
+ @Override
+ public ViewOutlineProvider getOutlineProvider() {
+ return super.getOutlineProvider();
+ }
+
+ public void setOutsideTouchListener(OnClickListener onClickListener) {
+ requestLayout();
+ setOnClickListener(onClickListener);
+ setClickable(true);
+ setFocusable(true);
+ }
+
+ public static VolumeUiLayout get(View v) {
+ if (v instanceof VolumeUiLayout) return (VolumeUiLayout) v;
+ if (v.getParent() instanceof View) {
+ return get((View) v.getParent());
+ }
+ return null;
+ }
+
+ private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = inoutInfo -> {
+ inoutInfo.setTouchableInsets(
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
+ };
+}
diff --git a/com/android/systemui/volume/car/CarVolumeDialogController.java b/com/android/systemui/volume/car/CarVolumeDialogController.java
index d28e42e6..474085c7 100644
--- a/com/android/systemui/volume/car/CarVolumeDialogController.java
+++ b/com/android/systemui/volume/car/CarVolumeDialogController.java
@@ -29,6 +29,8 @@ import com.android.systemui.volume.VolumeDialogControllerImpl;
/**
* A volume dialog controller for the automotive use case.
+ * TODO(hwwang): consider removing this class since it's coupled with stream_type and we are
+ * moving to use AudioAttributes usage for volume control in a car.
*
* {@link android.car.media.CarAudioManager} is the source of truth to get the stream volumes.
* And volume changes should be sent to the car's audio module instead of the android's audio mixer.
@@ -70,7 +72,7 @@ public class CarVolumeDialogController extends VolumeDialogControllerImpl {
return;
}
try {
- mCarAudioManager.setStreamVolume(stream, level, flag);
+ mCarAudioManager.setUsageVolume(stream, level, flag);
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car is not connected", e);
}
@@ -84,7 +86,7 @@ public class CarVolumeDialogController extends VolumeDialogControllerImpl {
}
try {
- return mCarAudioManager.getStreamVolume(stream);
+ return mCarAudioManager.getUsageVolume(stream);
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car is not connected", e);
return 0;
@@ -99,7 +101,7 @@ public class CarVolumeDialogController extends VolumeDialogControllerImpl {
}
try {
- return mCarAudioManager.getStreamMaxVolume(stream);
+ return mCarAudioManager.getUsageMaxVolume(stream);
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car is not connected", e);
return 0;
@@ -114,7 +116,7 @@ public class CarVolumeDialogController extends VolumeDialogControllerImpl {
}
try {
- return mCarAudioManager.getStreamMinVolume(stream);
+ return mCarAudioManager.getUsageMinVolume(stream);
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car is not connected", e);
return 0;
diff --git a/com/example/android/nn/benchmark/NNBenchmark.java b/com/example/android/nn/benchmark/NNBenchmark.java
new file mode 100644
index 00000000..0d765b99
--- /dev/null
+++ b/com/example/android/nn/benchmark/NNBenchmark.java
@@ -0,0 +1,267 @@
+/*
+ * 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 com.example.android.nn.benchmark;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.TextView;
+
+public class NNBenchmark extends Activity {
+ protected static final String TAG = "NN_BENCHMARK";
+
+ private int mTestList[];
+ private float mTestResults[];
+ private String mTestInfo[];
+
+ private TextView mTextView;
+ private boolean mToggleLong;
+ private boolean mTogglePause;
+
+ // In demo mode this is used to count updates in the pipeline. It's
+ // incremented when work is submitted to RS and decremented when invalidate is
+ // called to display a result.
+ private boolean mDemoMode;
+
+ // Initialize the parameters for Instrumentation tests.
+ protected void prepareInstrumentationTest() {
+ mTestList = new int[1];
+ mTestResults = new float[1];
+ mTestInfo = new String[1];
+ mDemoMode = false;
+ mProcessor = new Processor(!mDemoMode);
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+ // Processor is a helper thread for running the work without
+ // blocking the UI thread.
+ class Processor extends Thread {
+
+ private float mLastResult;
+ private boolean mRun = true;
+ private boolean mDoingBenchmark;
+ private NNTestBase mTest;
+
+ private boolean mBenchmarkMode;
+
+ void runTest() {
+ mTest.runTest();
+ }
+
+ Processor(boolean benchmarkMode) {
+ mBenchmarkMode = benchmarkMode;
+ }
+
+ class Result {
+ float totalTime;
+ int iterations;
+ String testInfo;
+ }
+
+ // Method to retreive benchmark results for instrumentation tests.
+ float getInstrumentationResult(NNTestList.TestName t) {
+ mTest = changeTest(t);
+ Result r = getBenchmark();
+ return r.totalTime / r.iterations * 1000.f;
+ }
+
+ // Run one loop of kernels for at least the specified minimum time.
+ // The function returns the average time in ms for the test run
+ private Result runBenchmarkLoop(float minTime) {
+ Result r = new Result();
+ long start = java.lang.System.currentTimeMillis();
+
+ r.testInfo = mTest.getTestInfo();
+ do {
+ // Run the kernel
+ mTest.runTest();
+ r.iterations ++;
+
+ long current = java.lang.System.currentTimeMillis();
+ r.totalTime = (current - start) / 1000.f;
+ } while (r.totalTime < minTime);
+
+ return r;
+ }
+
+
+ // Get a benchmark result for a specific test
+ private Result getBenchmark() {
+ mDoingBenchmark = true;
+
+ long result = 0;
+ float runtime = 1.f;
+ if (mToggleLong) {
+ runtime = 10.f;
+ }
+
+ // We run a short bit of work before starting the actual test
+ // this is to let any power management do its job and respond
+ runBenchmarkLoop(0.3f);
+
+ // Run the actual benchmark
+ Result r = runBenchmarkLoop(runtime);
+
+ Log.v(TAG, "Test: time=" + r.totalTime +"s, iterations=" + r.iterations +
+ ", avg=" + r.totalTime / r.iterations * 1000.f + " " + r.testInfo);
+
+ mDoingBenchmark = false;
+ return r;
+ }
+
+ public void run() {
+ while (mRun) {
+ // Our loop for launching tests or benchmarks
+ synchronized(this) {
+ // We may have been asked to exit while waiting
+ if (!mRun) return;
+ }
+
+ if (mBenchmarkMode) {
+ // Loop over the tests we want to benchmark
+ for (int ct=0; (ct < mTestList.length) && mRun; ct++) {
+
+ // For reproducibility we wait a short time for any sporadic work
+ // created by the user touching the screen to launch the test to pass.
+ // Also allows for things to settle after the test changes.
+ try {
+ sleep(250);
+ } catch(InterruptedException e) {
+ }
+
+ // If we just ran a test, we destroy it here to relieve some memory pressure
+ if (mTest != null) {
+ mTest.destroy();
+ }
+
+ // Select the next test
+ mTest = changeTest(mTestList[ct]);
+ // If the user selected the "long pause" option, wait
+ if (mTogglePause) {
+ for (int i=0; (i < 100) && mRun; i++) {
+ try {
+ sleep(100);
+ } catch(InterruptedException e) {
+ }
+ }
+ }
+
+ // Run the test
+ Result r = getBenchmark();
+ mTestResults[ct] = r.totalTime / r.iterations * 1000.f;
+ mTestInfo[ct] = r.testInfo;
+ }
+ onBenchmarkFinish(mRun);
+ } else {
+ // Run the kernel
+ runTest();
+ }
+ }
+
+ }
+
+ public void exit() {
+ mRun = false;
+
+ synchronized(this) {
+ notifyAll();
+ }
+
+ try {
+ this.join();
+ } catch(InterruptedException e) {
+ }
+
+ if (mTest != null) {
+ mTest.destroy();
+ mTest = null;
+ }
+ }
+ }
+
+
+ private boolean mDoingBenchmark;
+ public Processor mProcessor;
+
+ NNTestBase changeTest(NNTestList.TestName t) {
+ NNTestBase tb = NNTestList.newTest(t);
+ tb.createBaseTest(this);
+ return tb;
+ }
+
+ NNTestBase changeTest(int id) {
+ NNTestList.TestName t = NNTestList.TestName.values()[id];
+ return changeTest(t);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ TextView textView = new TextView(this);
+ textView.setTextSize(20);
+ textView.setText("NN BenchMark Running.");
+ setContentView(textView);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mProcessor != null) {
+ mProcessor.exit();
+ }
+ }
+
+ public void onBenchmarkFinish(boolean ok) {
+ if (ok) {
+ Intent intent = new Intent();
+ intent.putExtra("tests", mTestList);
+ intent.putExtra("results", mTestResults);
+ intent.putExtra("testinfo", mTestInfo);
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Intent i = getIntent();
+ mTestList = i.getIntArrayExtra("tests");
+
+ mToggleLong = i.getBooleanExtra("enable long", false);
+ mTogglePause = i.getBooleanExtra("enable pause", false);
+ mDemoMode = i.getBooleanExtra("demo", false);
+
+ if (mTestList != null) {
+ mTestResults = new float[mTestList.length];
+ mTestInfo = new String[mTestList.length];
+ mProcessor = new Processor(!mDemoMode);
+ if (mDemoMode) {
+ mProcessor.mTest = changeTest(mTestList[0]);
+ }
+ mProcessor.start();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+}
diff --git a/com/example/android/nn/benchmark/NNControls.java b/com/example/android/nn/benchmark/NNControls.java
new file mode 100644
index 00000000..05a4bb3f
--- /dev/null
+++ b/com/example/android/nn/benchmark/NNControls.java
@@ -0,0 +1,225 @@
+/*
+ * 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 com.example.android.nn.benchmark;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class NNControls extends Activity {
+ public final String RESULT_FILE = "nn_benchmark_result.csv";
+
+ private ListView mTestListView;
+ private TextView mResultView;
+
+ private ArrayAdapter<String> mTestListAdapter;
+ private ArrayList<String> mTestList = new ArrayList<String>();
+
+ private boolean mSettings[] = {false, false};
+ private static final int SETTING_LONG_RUN = 0;
+ private static final int SETTING_PAUSE = 1;
+
+ private float mResults[];
+ private String mInfo[];
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu items for use in the action bar
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main_activity_actions, menu);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ void init() {
+ for (int i=0; i < NNTestList.TestName.values().length; i++) {
+ mTestList.add(NNTestList.TestName.values()[i].toString());
+ }
+
+ mTestListView = findViewById(R.id.test_list);
+ mTestListAdapter = new ArrayAdapter(this,
+ android.R.layout.simple_list_item_activated_1,
+ mTestList);
+
+ mTestListView.setAdapter(mTestListAdapter);
+ mTestListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ mTestListAdapter.notifyDataSetChanged();
+
+ mResultView = findViewById(R.id.results);
+ mResultView.setMovementMethod(new ScrollingMovementMethod());
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.controls);
+ init();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+
+ Intent makeBasicLaunchIntent() {
+ Intent intent = new Intent(this, NNBenchmark.class);
+ intent.putExtra("enable long", mSettings[SETTING_LONG_RUN]);
+ intent.putExtra("enable pause", mSettings[SETTING_PAUSE]);
+ return intent;
+ }
+
+ public void btnRun(View v) {
+ NNTestList.TestName t[] = NNTestList.TestName.values();
+ int count = 0;
+ for (int i = 0; i < t.length; i++) {
+ if (mTestListView.isItemChecked(i)) {
+ count++;
+ }
+ }
+ if (count == 0) {
+ return;
+ }
+
+ int testList[] = new int[count];
+ count = 0;
+ for (int i = 0; i < t.length; i++) {
+ if (mTestListView.isItemChecked(i)) {
+ testList[count++] = i;
+ }
+ }
+
+ Intent intent = makeBasicLaunchIntent();
+ intent.putExtra("tests", testList);
+ startActivityForResult(intent, 0);
+ }
+
+ float rebase(float v, NNTestList.TestName t) {
+ if (v > 0.001) {
+ v = t.baseline / v;
+ }
+ return v;
+ }
+
+
+ private void writeResults() {
+ // write result into a file
+ File externalStorage = Environment.getExternalStorageDirectory();
+ if (!externalStorage.canWrite()) {
+ Log.v(NNBenchmark.TAG, "sdcard is not writable");
+ return;
+ }
+ File resultFile = new File(externalStorage, RESULT_FILE);
+ resultFile.setWritable(true, false);
+ try {
+ BufferedWriter rsWriter = new BufferedWriter(new FileWriter(resultFile));
+ Log.v(NNBenchmark.TAG, "Saved results in: " + resultFile.getAbsolutePath());
+ java.text.DecimalFormat df = new java.text.DecimalFormat("######.##");
+
+ for (int ct=0; ct < NNTestList.TestName.values().length; ct++) {
+ NNTestList.TestName t = NNTestList.TestName.values()[ct];
+ final float r = mResults[ct];
+ float r2 = rebase(r, t);
+ String s = new String("" + t.toString() + ", " + df.format(r) + ", " +
+ df.format(r2));
+ rsWriter.write(s + "\n");
+ }
+ rsWriter.close();
+ } catch (IOException e) {
+ Log.v(NNBenchmark.TAG, "Unable to write result file " + e.getMessage());
+ }
+ }
+
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == 0) {
+ if (resultCode == RESULT_OK) {
+ java.text.DecimalFormat df = new java.text.DecimalFormat("######.##");
+ mResults = new float[NNTestList.TestName.values().length];
+ mInfo = new String[NNTestList.TestName.values().length];
+
+ float r[] = data.getFloatArrayExtra("results");
+ String inf[] = data.getStringArrayExtra("testinfo");
+ int id[] = data.getIntArrayExtra("tests");
+
+ String mOutResult = "";
+ for (int ct=0; ct < id.length; ct++) {
+ NNTestList.TestName t = NNTestList.TestName.values()[id[ct]];
+ String s = t.toString() + " " + df.format(rebase(r[ct], t)) +
+ "X, " + df.format(r[ct]) + "ms";
+ mTestList.set(id[ct], s);
+ mTestListAdapter.notifyDataSetChanged();
+ mOutResult += s + '\n';
+ mResults[id[ct]] = r[ct];
+ }
+
+ mResultView.setText(mOutResult);
+ writeResults();
+ }
+ }
+ }
+
+ public void btnSelAll(View v) {
+ NNTestList.TestName t[] = NNTestList.TestName.values();
+ for (int i=0; i < t.length; i++) {
+ mTestListView.setItemChecked(i, true);
+ }
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle presses on the action bar items
+ switch(item.getItemId()) {
+ case R.id.action_settings:
+ NNSettings newFragment = new NNSettings(mSettings);
+ newFragment.show(getFragmentManager(), "settings");
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void btnSelNone(View v) {
+ NNTestList.TestName t[] = NNTestList.TestName.values();
+ for (int i=0; i < t.length; i++) {
+ mTestListView.setItemChecked(i, false);
+ }
+ }
+
+ public void btnSettings(View v) {
+ NNSettings newFragment = new NNSettings(mSettings);
+ newFragment.show(getFragmentManager(), "settings");
+ }
+}
diff --git a/com/example/android/nn/benchmark/NNSettings.java b/com/example/android/nn/benchmark/NNSettings.java
new file mode 100644
index 00000000..687a268d
--- /dev/null
+++ b/com/example/android/nn/benchmark/NNSettings.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.example.android.nn.benchmark;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+
+public class NNSettings extends DialogFragment {
+ private boolean[] mEnables;
+ public boolean mOk = false;
+
+ public NNSettings(boolean[] enables) {
+ mEnables = enables;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.settings);
+
+ // Specify the list array, the items to be selected by default (null for none),
+ // and the listener through which to receive callbacks when items are selected
+ builder.setMultiChoiceItems(R.array.settings_array, mEnables,
+ new DialogInterface.OnMultiChoiceClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which, boolean isChecked) {
+ mEnables[which] = isChecked;
+ }
+ });
+
+ // Set the action buttons
+ builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ mOk = true;
+ }
+ });
+ builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ }
+ });
+
+ return builder.create();
+ }
+}
diff --git a/com/example/android/nn/benchmark/NNTest.java b/com/example/android/nn/benchmark/NNTest.java
new file mode 100644
index 00000000..f9116974
--- /dev/null
+++ b/com/example/android/nn/benchmark/NNTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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 com.example.android.nn.benchmark;
+
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import com.example.android.nn.benchmark.NNTestList.TestName;
+
+/**
+ * NNAPI benchmark test.
+ * To run the test, please use command
+ *
+ * adb shell am instrument -w com.example.android.nn.benchmark/android.support.test.runner.AndroidJUnitRunner
+ *
+ */
+public class NNTest extends ActivityInstrumentationTestCase2<NNBenchmark> {
+ // Only run 1 iteration now to fit the MediumTest time requirement.
+ // One iteration means running the tests continuous for 1s.
+ private int mIteration = 1;
+ private NNBenchmark mActivity;
+
+ public NNTest() {
+ super(NNBenchmark.class);
+ }
+
+ // Initialize the parameter for ImageProcessingActivityJB.
+ protected void prepareTest() {
+ mActivity = getActivity();
+ mActivity.prepareInstrumentationTest();
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ prepareTest();
+ setActivityInitialTouchMode(false);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ class TestAction implements Runnable {
+ TestName mTestName;
+ float mResult;
+ public TestAction(TestName testName) {
+ mTestName = testName;
+ }
+ public void run() {
+ mResult = mActivity.mProcessor.getInstrumentationResult(mTestName);
+ Log.v(NNBenchmark.TAG,
+ "Benchmark for test \"" + mTestName.toString() + "\" is: " + mResult);
+ synchronized(this) {
+ this.notify();
+ }
+ }
+ public float getBenchmark() {
+ return mResult;
+ }
+ }
+
+ // Set the benchmark thread to run on ui thread
+ // Synchronized the thread such that the test will wait for the benchmark thread to finish
+ public void runOnUiThread(Runnable action) {
+ synchronized(action) {
+ mActivity.runOnUiThread(action);
+ try {
+ action.wait();
+ } catch (InterruptedException e) {
+ Log.v(NNBenchmark.TAG, "waiting for action running on UI thread is interrupted: " +
+ e.toString());
+ }
+ }
+ }
+
+ public void runTest(TestAction ta, String testName) {
+ float sum = 0;
+ for (int i = 0; i < mIteration; i++) {
+ runOnUiThread(ta);
+ float bmValue = ta.getBenchmark();
+ Log.v(NNBenchmark.TAG, "results for iteration " + i + " is " + bmValue);
+ sum += bmValue;
+ }
+ float avgResult = sum/mIteration;
+
+ // post result to INSTRUMENTATION_STATUS
+ Bundle results = new Bundle();
+ results.putFloat(testName + "_avg", avgResult);
+ getInstrumentation().sendStatus(Activity.RESULT_OK, results);
+ }
+
+ // Test case 0: MobileNet float32
+ @MediumTest
+ public void testMobileNetFloat() {
+ TestAction ta = new TestAction(TestName.MobileNet_FLOAT);
+ runTest(ta, TestName.MobileNet_FLOAT.name());
+ }
+
+ // Test case 1: MobileNet quantized
+ @MediumTest
+ public void testMobileNetQuantized() {
+ TestAction ta = new TestAction(TestName.MobileNet_QUANT8);
+ runTest(ta, TestName.MobileNet_QUANT8.name());
+ }
+} \ No newline at end of file
diff --git a/com/example/android/nn/benchmark/NNTestBase.java b/com/example/android/nn/benchmark/NNTestBase.java
new file mode 100644
index 00000000..d2e8dab2
--- /dev/null
+++ b/com/example/android/nn/benchmark/NNTestBase.java
@@ -0,0 +1,111 @@
+/*
+ * 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 com.example.android.nn.benchmark;
+
+import android.content.res.AssetManager;
+import android.util.Log;
+import android.widget.TextView;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class NNTestBase {
+ protected final boolean USE_NNAPI = true;
+
+ // Used to load the 'native-lib' library on application startup.
+ static {
+ System.loadLibrary("nnbenchmark");
+ }
+
+ private synchronized native long initModel(String modelFileName);
+ private synchronized native void destroyModel(long modelHandle);
+ private synchronized native boolean resizeInputTensors(long modelHandle, int[] inputShape);
+ private synchronized native boolean runBenchmark(long modelHandle, boolean useNNAPI);
+
+ protected NNBenchmark mActivity;
+ protected TextView mText;
+ private String mModelName;
+ private long mModelHandle;
+ private int[] mInputShape;
+
+ public NNTestBase(String modelName, int[] inputShape) {
+ mModelName = modelName;
+ mInputShape = inputShape;
+ mModelHandle = 0;
+ }
+
+ public final void createBaseTest(NNBenchmark ipact) {
+ mActivity = ipact;
+ String modelFileName = copyAssetToFile();
+ if (modelFileName != null) {
+ mModelHandle = initModel(modelFileName);
+ if (mModelHandle != 0) {
+ resizeInputTensors(mModelHandle, mInputShape);
+ } else {
+ Log.e(NNBenchmark.TAG, "Failed to init the model");
+ }
+ }
+ }
+
+ public String getTestInfo() {
+ return mModelName;
+ }
+
+ public void runTest() {
+ if (mModelHandle != 0) {
+ runBenchmark(mModelHandle, USE_NNAPI);
+ }
+ }
+
+ public void destroy() {
+ if (mModelHandle != 0) {
+ destroyModel(mModelHandle);
+ mModelHandle = 0;
+ }
+ }
+
+ // We need to copy it to cache dir, so that TFlite can load it directly.
+ private String copyAssetToFile() {
+ String outFileName;
+ String modelAssetName = mModelName + ".tflite";
+ AssetManager assetManager = mActivity.getAssets();
+ try {
+ InputStream in = assetManager.open(modelAssetName);
+
+ outFileName = mActivity.getCacheDir().getAbsolutePath() + "/" + modelAssetName;
+ File outFile = new File(outFileName);
+ OutputStream out = new FileOutputStream(outFile);
+
+ byte[] buffer = new byte[1024];
+ int read;
+ while((read = in.read(buffer)) != -1){
+ out.write(buffer, 0, read);
+ }
+ out.flush();
+
+ in.close();
+ out.close();
+ } catch(IOException e) {
+ Log.e(NNBenchmark.TAG, "Failed to copy asset file: " + modelAssetName, e);
+ return null;
+ }
+ return outFileName;
+ }
+}
diff --git a/com/example/android/nn/benchmark/NNTestList.java b/com/example/android/nn/benchmark/NNTestList.java
new file mode 100644
index 00000000..39b4e59d
--- /dev/null
+++ b/com/example/android/nn/benchmark/NNTestList.java
@@ -0,0 +1,57 @@
+/*
+ * 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 com.example.android.nn.benchmark;
+
+public class NNTestList {
+ /**
+ * Define enum type for test names
+ */
+ public enum TestName {
+
+ MobileNet_FLOAT ("MobileNet float32", 160.f),
+ MobileNet_QUANT8 ("MobileNet quantized", 50.f);
+
+ private final String name;
+ public final float baseline;
+
+ private TestName(String s, float base) {
+ name = s;
+ baseline = base;
+ }
+ private TestName(String s) {
+ name = s;
+ baseline = 1.f;
+ }
+
+ // return quoted string as displayed test name
+ public String toString() {
+ return name;
+ }
+ }
+
+ static NNTestBase newTest(TestName testName) {
+ switch(testName) {
+ case MobileNet_FLOAT:
+ return new NNTestBase("mobilenet_float", new int[]{1, 224, 224, 3});
+ case MobileNet_QUANT8:
+ return new NNTestBase("mobilenet_quantized", new int[]{1, 224, 224, 3});
+ default:
+ return null;
+ }
+ }
+}
+
diff --git a/foo/bar/ComplexDao.java b/foo/bar/ComplexDao.java
index 89859e50..dbb54fbc 100644
--- a/foo/bar/ComplexDao.java
+++ b/foo/bar/ComplexDao.java
@@ -1,69 +1,433 @@
-/*
- * 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 foo.bar;
-import android.arch.persistence.room.*;
-import java.util.List;
+
+import android.arch.lifecycle.ComputableLiveData;
import android.arch.lifecycle.LiveData;
-@Dao
-abstract class ComplexDao {
- static class FullName {
- public int id;
- public String fullName;
- }
+import android.arch.persistence.room.InvalidationTracker.Observer;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.RoomSQLiteQuery;
+import android.arch.persistence.room.util.StringUtil;
+import android.database.Cursor;
+import android.support.annotation.NonNull;
+import java.lang.Integer;
+import java.lang.Override;
+import java.lang.String;
+import java.lang.StringBuilder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Generated;
- private final ComplexDatabase mDb;
+@Generated("android.arch.persistence.room.RoomProcessor")
+public class ComplexDao_Impl extends ComplexDao {
+ private final RoomDatabase __db;
- public ComplexDao(ComplexDatabase db) {
- mDb = db;
+ public ComplexDao_Impl(ComplexDatabase __db) {
+ super(__db);
+ this.__db = __db;
}
- @Transaction
+ @Override
public boolean transactionMethod(int i, String s, long l) {
- return true;
+ __db.beginTransaction();
+ try {
+ boolean _result = super.transactionMethod(i, s, l);
+ __db.setTransactionSuccessful();
+ return _result;
+ } finally {
+ __db.endTransaction();
+ }
}
- @Query("SELECT name || lastName as fullName, uid as id FROM user where uid = :id")
- abstract public List<FullName> fullNames(int id);
+ @Override
+ public List<ComplexDao.FullName> fullNames(int id) {
+ final String _sql = "SELECT name || lastName as fullName, uid as id FROM user where uid = ?";
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
+ int _argIndex = 1;
+ _statement.bindLong(_argIndex, id);
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final int _cursorIndexOfFullName = _cursor.getColumnIndexOrThrow("fullName");
+ final int _cursorIndexOfId = _cursor.getColumnIndexOrThrow("id");
+ final List<ComplexDao.FullName> _result = new ArrayList<ComplexDao.FullName>(_cursor.getCount());
+ while(_cursor.moveToNext()) {
+ final ComplexDao.FullName _item;
+ _item = new ComplexDao.FullName();
+ _item.fullName = _cursor.getString(_cursorIndexOfFullName);
+ _item.id = _cursor.getInt(_cursorIndexOfId);
+ _result.add(_item);
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ _statement.release();
+ }
+ }
- @Query("SELECT * FROM user where uid = :id")
- abstract public User getById(int id);
+ @Override
+ public User getById(int id) {
+ final String _sql = "SELECT * FROM user where uid = ?";
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
+ int _argIndex = 1;
+ _statement.bindLong(_argIndex, id);
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+ final User _result;
+ if(_cursor.moveToFirst()) {
+ _result = new User();
+ _result.uid = _cursor.getInt(_cursorIndexOfUid);
+ _result.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _result.setLastName(_tmpLastName);
+ _result.age = _cursor.getInt(_cursorIndexOfAge);
+ } else {
+ _result = null;
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ _statement.release();
+ }
+ }
- @Query("SELECT * FROM user where name LIKE :name AND lastName LIKE :lastName")
- abstract public User findByName(String name, String lastName);
+ @Override
+ public User findByName(String name, String lastName) {
+ final String _sql = "SELECT * FROM user where name LIKE ? AND lastName LIKE ?";
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 2);
+ int _argIndex = 1;
+ if (name == null) {
+ _statement.bindNull(_argIndex);
+ } else {
+ _statement.bindString(_argIndex, name);
+ }
+ _argIndex = 2;
+ if (lastName == null) {
+ _statement.bindNull(_argIndex);
+ } else {
+ _statement.bindString(_argIndex, lastName);
+ }
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+ final User _result;
+ if(_cursor.moveToFirst()) {
+ _result = new User();
+ _result.uid = _cursor.getInt(_cursorIndexOfUid);
+ _result.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _result.setLastName(_tmpLastName);
+ _result.age = _cursor.getInt(_cursorIndexOfAge);
+ } else {
+ _result = null;
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ _statement.release();
+ }
+ }
- @Query("SELECT * FROM user where uid IN (:ids)")
- abstract public List<User> loadAllByIds(int... ids);
+ @Override
+ public List<User> loadAllByIds(int... ids) {
+ StringBuilder _stringBuilder = StringUtil.newStringBuilder();
+ _stringBuilder.append("SELECT * FROM user where uid IN (");
+ final int _inputSize = ids.length;
+ StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(")");
+ final String _sql = _stringBuilder.toString();
+ final int _argCount = 0 + _inputSize;
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, _argCount);
+ int _argIndex = 1;
+ for (int _item : ids) {
+ _statement.bindLong(_argIndex, _item);
+ _argIndex ++;
+ }
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+ final List<User> _result = new ArrayList<User>(_cursor.getCount());
+ while(_cursor.moveToNext()) {
+ final User _item_1;
+ _item_1 = new User();
+ _item_1.uid = _cursor.getInt(_cursorIndexOfUid);
+ _item_1.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _item_1.setLastName(_tmpLastName);
+ _item_1.age = _cursor.getInt(_cursorIndexOfAge);
+ _result.add(_item_1);
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ _statement.release();
+ }
+ }
- @Query("SELECT ageColumn FROM user where uid = :id")
- abstract int getAge(int id);
+ @Override
+ int getAge(int id) {
+ final String _sql = "SELECT ageColumn FROM user where uid = ?";
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
+ int _argIndex = 1;
+ _statement.bindLong(_argIndex, id);
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final int _result;
+ if(_cursor.moveToFirst()) {
+ _result = _cursor.getInt(0);
+ } else {
+ _result = 0;
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ _statement.release();
+ }
+ }
- @Query("SELECT ageColumn FROM user where uid IN(:ids)")
- abstract public int[] getAllAges(int... ids);
+ @Override
+ public int[] getAllAges(int... ids) {
+ StringBuilder _stringBuilder = StringUtil.newStringBuilder();
+ _stringBuilder.append("SELECT ageColumn FROM user where uid IN(");
+ final int _inputSize = ids.length;
+ StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(")");
+ final String _sql = _stringBuilder.toString();
+ final int _argCount = 0 + _inputSize;
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, _argCount);
+ int _argIndex = 1;
+ for (int _item : ids) {
+ _statement.bindLong(_argIndex, _item);
+ _argIndex ++;
+ }
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final int[] _result = new int[_cursor.getCount()];
+ int _index = 0;
+ while(_cursor.moveToNext()) {
+ final int _item_1;
+ _item_1 = _cursor.getInt(0);
+ _result[_index] = _item_1;
+ _index ++;
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ _statement.release();
+ }
+ }
+
+ @Override
+ public List<Integer> getAllAgesAsList(List<Integer> ids) {
+ StringBuilder _stringBuilder = StringUtil.newStringBuilder();
+ _stringBuilder.append("SELECT ageColumn FROM user where uid IN(");
+ final int _inputSize = ids.size();
+ StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(")");
+ final String _sql = _stringBuilder.toString();
+ final int _argCount = 0 + _inputSize;
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, _argCount);
+ int _argIndex = 1;
+ for (Integer _item : ids) {
+ if (_item == null) {
+ _statement.bindNull(_argIndex);
+ } else {
+ _statement.bindLong(_argIndex, _item);
+ }
+ _argIndex ++;
+ }
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final List<Integer> _result = new ArrayList<Integer>(_cursor.getCount());
+ while(_cursor.moveToNext()) {
+ final Integer _item_1;
+ if (_cursor.isNull(0)) {
+ _item_1 = null;
+ } else {
+ _item_1 = _cursor.getInt(0);
+ }
+ _result.add(_item_1);
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ _statement.release();
+ }
+ }
- @Query("SELECT ageColumn FROM user where uid IN(:ids)")
- abstract public List<Integer> getAllAgesAsList(List<Integer> ids);
+ @Override
+ public LiveData<User> getByIdLive(int id) {
+ final String _sql = "SELECT * FROM user where uid = ?";
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
+ int _argIndex = 1;
+ _statement.bindLong(_argIndex, id);
+ return new ComputableLiveData<User>() {
+ private Observer _observer;
- @Query("SELECT * FROM user where uid = :id")
- abstract public LiveData<User> getByIdLive(int id);
+ @Override
+ protected User compute() {
+ if (_observer == null) {
+ _observer = new Observer("user") {
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ invalidate();
+ }
+ };
+ __db.getInvalidationTracker().addWeakObserver(_observer);
+ }
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+ final User _result;
+ if(_cursor.moveToFirst()) {
+ _result = new User();
+ _result.uid = _cursor.getInt(_cursorIndexOfUid);
+ _result.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _result.setLastName(_tmpLastName);
+ _result.age = _cursor.getInt(_cursorIndexOfAge);
+ } else {
+ _result = null;
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ }
+ }
+
+ @Override
+ protected void finalize() {
+ _statement.release();
+ }
+ }.getLiveData();
+ }
- @Query("SELECT * FROM user where uid IN (:ids)")
- abstract public LiveData<List<User>> loadUsersByIdsLive(int... ids);
+ @Override
+ public LiveData<List<User>> loadUsersByIdsLive(int... ids) {
+ StringBuilder _stringBuilder = StringUtil.newStringBuilder();
+ _stringBuilder.append("SELECT * FROM user where uid IN (");
+ final int _inputSize = ids.length;
+ StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(")");
+ final String _sql = _stringBuilder.toString();
+ final int _argCount = 0 + _inputSize;
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, _argCount);
+ int _argIndex = 1;
+ for (int _item : ids) {
+ _statement.bindLong(_argIndex, _item);
+ _argIndex ++;
+ }
+ return new ComputableLiveData<List<User>>() {
+ private Observer _observer;
- @Query("SELECT ageColumn FROM user where uid IN(:ids1) OR uid IN (:ids2) OR uid IN (:ids3)")
- abstract public List<Integer> getAllAgesAsList(List<Integer> ids1,
- int[] ids2, int... ids3);
+ @Override
+ protected List<User> compute() {
+ if (_observer == null) {
+ _observer = new Observer("user") {
+ @Override
+ public void onInvalidated(@NonNull Set<String> tables) {
+ invalidate();
+ }
+ };
+ __db.getInvalidationTracker().addWeakObserver(_observer);
+ }
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final int _cursorIndexOfUid = _cursor.getColumnIndexOrThrow("uid");
+ final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
+ final int _cursorIndexOfLastName = _cursor.getColumnIndexOrThrow("lastName");
+ final int _cursorIndexOfAge = _cursor.getColumnIndexOrThrow("ageColumn");
+ final List<User> _result = new ArrayList<User>(_cursor.getCount());
+ while(_cursor.moveToNext()) {
+ final User _item_1;
+ _item_1 = new User();
+ _item_1.uid = _cursor.getInt(_cursorIndexOfUid);
+ _item_1.name = _cursor.getString(_cursorIndexOfName);
+ final String _tmpLastName;
+ _tmpLastName = _cursor.getString(_cursorIndexOfLastName);
+ _item_1.setLastName(_tmpLastName);
+ _item_1.age = _cursor.getInt(_cursorIndexOfAge);
+ _result.add(_item_1);
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ }
+ }
+
+ @Override
+ protected void finalize() {
+ _statement.release();
+ }
+ }.getLiveData();
+ }
+
+ @Override
+ public List<Integer> getAllAgesAsList(List<Integer> ids1, int[] ids2, int... ids3) {
+ StringBuilder _stringBuilder = StringUtil.newStringBuilder();
+ _stringBuilder.append("SELECT ageColumn FROM user where uid IN(");
+ final int _inputSize = ids1.size();
+ StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(") OR uid IN (");
+ final int _inputSize_1 = ids2.length;
+ StringUtil.appendPlaceholders(_stringBuilder, _inputSize_1);
+ _stringBuilder.append(") OR uid IN (");
+ final int _inputSize_2 = ids3.length;
+ StringUtil.appendPlaceholders(_stringBuilder, _inputSize_2);
+ _stringBuilder.append(")");
+ final String _sql = _stringBuilder.toString();
+ final int _argCount = 0 + _inputSize + _inputSize_1 + _inputSize_2;
+ final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, _argCount);
+ int _argIndex = 1;
+ for (Integer _item : ids1) {
+ if (_item == null) {
+ _statement.bindNull(_argIndex);
+ } else {
+ _statement.bindLong(_argIndex, _item);
+ }
+ _argIndex ++;
+ }
+ _argIndex = 1 + _inputSize;
+ for (int _item_1 : ids2) {
+ _statement.bindLong(_argIndex, _item_1);
+ _argIndex ++;
+ }
+ _argIndex = 1 + _inputSize + _inputSize_1;
+ for (int _item_2 : ids3) {
+ _statement.bindLong(_argIndex, _item_2);
+ _argIndex ++;
+ }
+ final Cursor _cursor = __db.query(_statement);
+ try {
+ final List<Integer> _result = new ArrayList<Integer>(_cursor.getCount());
+ while(_cursor.moveToNext()) {
+ final Integer _item_3;
+ if (_cursor.isNull(0)) {
+ _item_3 = null;
+ } else {
+ _item_3 = _cursor.getInt(0);
+ }
+ _result.add(_item_3);
+ }
+ return _result;
+ } finally {
+ _cursor.close();
+ _statement.release();
+ }
+ }
}
diff --git a/foo/bar/ComplexDatabase.java b/foo/bar/ComplexDatabase.java
index f35e0b8a..cfdc1101 100644
--- a/foo/bar/ComplexDatabase.java
+++ b/foo/bar/ComplexDatabase.java
@@ -1,23 +1,99 @@
-/*
- * 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 foo.bar;
-import android.arch.persistence.room.*;
-import java.util.List;
-@Database(entities = {User.class}, version = 1923)
-abstract class ComplexDatabase extends RoomDatabase {
- abstract ComplexDao getComplexDao();
+
+import android.arch.persistence.db.SupportSQLiteDatabase;
+import android.arch.persistence.db.SupportSQLiteOpenHelper;
+import android.arch.persistence.db.SupportSQLiteOpenHelper.Callback;
+import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration;
+import android.arch.persistence.room.DatabaseConfiguration;
+import android.arch.persistence.room.InvalidationTracker;
+import android.arch.persistence.room.RoomOpenHelper;
+import android.arch.persistence.room.RoomOpenHelper.Delegate;
+import android.arch.persistence.room.util.TableInfo;
+import android.arch.persistence.room.util.TableInfo.Column;
+import android.arch.persistence.room.util.TableInfo.ForeignKey;
+import android.arch.persistence.room.util.TableInfo.Index;
+import java.lang.IllegalStateException;
+import java.lang.Override;
+import java.lang.String;
+import java.util.HashMap;
+import java.util.HashSet;
+import javax.annotation.Generated;
+
+@Generated("android.arch.persistence.room.RoomProcessor")
+public class ComplexDatabase_Impl extends ComplexDatabase {
+ private volatile ComplexDao _complexDao;
+
+ protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
+ final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(1923) {
+ public void createAllTables(SupportSQLiteDatabase _db) {
+ _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER NOT NULL, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER NOT NULL, PRIMARY KEY(`uid`))");
+ _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
+ _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6773601c5bcf94c71ee4eb0de04f21a4\")");
+ }
+
+ public void dropAllTables(SupportSQLiteDatabase _db) {
+ _db.execSQL("DROP TABLE IF EXISTS `User`");
+ }
+
+ protected void onCreate(SupportSQLiteDatabase _db) {
+ if (mCallbacks != null) {
+ for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
+ mCallbacks.get(_i).onCreate(_db);
+ }
+ }
+ }
+
+ public void onOpen(SupportSQLiteDatabase _db) {
+ mDatabase = _db;
+ internalInitInvalidationTracker(_db);
+ if (mCallbacks != null) {
+ for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
+ mCallbacks.get(_i).onOpen(_db);
+ }
+ }
+ }
+
+ protected void validateMigration(SupportSQLiteDatabase _db) {
+ final HashMap<String, TableInfo.Column> _columnsUser = new HashMap<String, TableInfo.Column>(4);
+ _columnsUser.put("uid", new TableInfo.Column("uid", "INTEGER", true, 1));
+ _columnsUser.put("name", new TableInfo.Column("name", "TEXT", false, 0));
+ _columnsUser.put("lastName", new TableInfo.Column("lastName", "TEXT", false, 0));
+ _columnsUser.put("ageColumn", new TableInfo.Column("ageColumn", "INTEGER", true, 0));
+ final HashSet<TableInfo.ForeignKey> _foreignKeysUser = new HashSet<TableInfo.ForeignKey>(0);
+ final HashSet<TableInfo.Index> _indicesUser = new HashSet<TableInfo.Index>(0);
+ final TableInfo _infoUser = new TableInfo("User", _columnsUser, _foreignKeysUser, _indicesUser);
+ final TableInfo _existingUser = TableInfo.read(_db, "User");
+ if (! _infoUser.equals(_existingUser)) {
+ throw new IllegalStateException("Migration didn't properly handle User(foo.bar.User).\n"
+ + " Expected:\n" + _infoUser + "\n"
+ + " Found:\n" + _existingUser);
+ }
+ }
+ }, "6773601c5bcf94c71ee4eb0de04f21a4");
+ final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
+ .name(configuration.name)
+ .callback(_openCallback)
+ .build();
+ final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
+ return _helper;
+ }
+
+ @Override
+ protected InvalidationTracker createInvalidationTracker() {
+ return new InvalidationTracker(this, "User");
+ }
+
+ @Override
+ ComplexDao getComplexDao() {
+ if (_complexDao != null) {
+ return _complexDao;
+ } else {
+ synchronized(this) {
+ if(_complexDao == null) {
+ _complexDao = new ComplexDao_Impl(this);
+ }
+ return _complexDao;
+ }
+ }
+ }
}
diff --git a/foo/bar/DeletionDao.java b/foo/bar/DeletionDao.java
index 997f2906..067bf670 100644
--- a/foo/bar/DeletionDao.java
+++ b/foo/bar/DeletionDao.java
@@ -1,51 +1,240 @@
-/*
- * 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 foo.bar;
-import android.arch.persistence.room.*;
+
+import android.arch.persistence.db.SupportSQLiteStatement;
+import android.arch.persistence.room.EntityDeletionOrUpdateAdapter;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.SharedSQLiteStatement;
+import android.arch.persistence.room.util.StringUtil;
+import java.lang.Override;
+import java.lang.String;
+import java.lang.StringBuilder;
import java.util.List;
+import javax.annotation.Generated;
+
+@Generated("android.arch.persistence.room.RoomProcessor")
+public class DeletionDao_Impl implements DeletionDao {
+ private final RoomDatabase __db;
+
+ private final EntityDeletionOrUpdateAdapter __deletionAdapterOfUser;
+
+ private final EntityDeletionOrUpdateAdapter __deletionAdapterOfMultiPKeyEntity;
+
+ private final EntityDeletionOrUpdateAdapter __deletionAdapterOfBook;
+
+ private final SharedSQLiteStatement __preparedStmtOfDeleteByUid;
+
+ private final SharedSQLiteStatement __preparedStmtOfDeleteEverything;
+
+ public DeletionDao_Impl(RoomDatabase __db) {
+ this.__db = __db;
+ this.__deletionAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
+ @Override
+ public String createQuery() {
+ return "DELETE FROM `User` WHERE `uid` = ?";
+ }
+
+ @Override
+ public void bind(SupportSQLiteStatement stmt, User value) {
+ stmt.bindLong(1, value.uid);
+ }
+ };
+ this.__deletionAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
+ @Override
+ public String createQuery() {
+ return "DELETE FROM `MultiPKeyEntity` WHERE `name` = ? AND `lastName` = ?";
+ }
+
+ @Override
+ public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
+ if (value.name == null) {
+ stmt.bindNull(1);
+ } else {
+ stmt.bindString(1, value.name);
+ }
+ if (value.lastName == null) {
+ stmt.bindNull(2);
+ } else {
+ stmt.bindString(2, value.lastName);
+ }
+ }
+ };
+ this.__deletionAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
+ @Override
+ public String createQuery() {
+ return "DELETE FROM `Book` WHERE `bookId` = ?";
+ }
+
+ @Override
+ public void bind(SupportSQLiteStatement stmt, Book value) {
+ stmt.bindLong(1, value.bookId);
+ }
+ };
+ this.__preparedStmtOfDeleteByUid = new SharedSQLiteStatement(__db) {
+ @Override
+ public String createQuery() {
+ final String _query = "DELETE FROM user where uid = ?";
+ return _query;
+ }
+ };
+ this.__preparedStmtOfDeleteEverything = new SharedSQLiteStatement(__db) {
+ @Override
+ public String createQuery() {
+ final String _query = "DELETE FROM user";
+ return _query;
+ }
+ };
+ }
+
+ @Override
+ public void deleteUser(User user) {
+ __db.beginTransaction();
+ try {
+ __deletionAdapterOfUser.handle(user);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public void deleteUsers(User user1, List<User> others) {
+ __db.beginTransaction();
+ try {
+ __deletionAdapterOfUser.handle(user1);
+ __deletionAdapterOfUser.handleMultiple(others);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public void deleteArrayOfUsers(User[] users) {
+ __db.beginTransaction();
+ try {
+ __deletionAdapterOfUser.handleMultiple(users);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public int deleteUserAndReturnCount(User user) {
+ int _total = 0;
+ __db.beginTransaction();
+ try {
+ _total +=__deletionAdapterOfUser.handle(user);
+ __db.setTransactionSuccessful();
+ return _total;
+ } finally {
+ __db.endTransaction();
+ }
+ }
-@Dao
-abstract interface DeletionDao {
- @Delete
- void deleteUser(User user);
- @Delete
- void deleteUsers(User user1, List<User> others);
- @Delete
- void deleteArrayOfUsers(User[] users);
+ @Override
+ public int deleteUserAndReturnCount(User user1, List<User> others) {
+ int _total = 0;
+ __db.beginTransaction();
+ try {
+ _total +=__deletionAdapterOfUser.handle(user1);
+ _total +=__deletionAdapterOfUser.handleMultiple(others);
+ __db.setTransactionSuccessful();
+ return _total;
+ } finally {
+ __db.endTransaction();
+ }
+ }
- @Delete
- int deleteUserAndReturnCount(User user);
- @Delete
- int deleteUserAndReturnCount(User user1, List<User> others);
- @Delete
- int deleteUserAndReturnCount(User[] users);
+ @Override
+ public int deleteUserAndReturnCount(User[] users) {
+ int _total = 0;
+ __db.beginTransaction();
+ try {
+ _total +=__deletionAdapterOfUser.handleMultiple(users);
+ __db.setTransactionSuccessful();
+ return _total;
+ } finally {
+ __db.endTransaction();
+ }
+ }
- @Delete
- int multiPKey(MultiPKeyEntity entity);
+ @Override
+ public int multiPKey(MultiPKeyEntity entity) {
+ int _total = 0;
+ __db.beginTransaction();
+ try {
+ _total +=__deletionAdapterOfMultiPKeyEntity.handle(entity);
+ __db.setTransactionSuccessful();
+ return _total;
+ } finally {
+ __db.endTransaction();
+ }
+ }
- @Query("DELETE FROM user where uid = :uid")
- int deleteByUid(int uid);
+ @Override
+ public void deleteUserAndBook(User user, Book book) {
+ __db.beginTransaction();
+ try {
+ __deletionAdapterOfUser.handle(user);
+ __deletionAdapterOfBook.handle(book);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
- @Query("DELETE FROM user where uid IN(:uid)")
- int deleteByUidList(int... uid);
+ @Override
+ public int deleteByUid(int uid) {
+ final SupportSQLiteStatement _stmt = __preparedStmtOfDeleteByUid.acquire();
+ __db.beginTransaction();
+ try {
+ int _argIndex = 1;
+ _stmt.bindLong(_argIndex, uid);
+ final int _result = _stmt.executeUpdateDelete();
+ __db.setTransactionSuccessful();
+ return _result;
+ } finally {
+ __db.endTransaction();
+ __preparedStmtOfDeleteByUid.release(_stmt);
+ }
+ }
- @Delete
- void deleteUserAndBook(User user, Book book);
+ @Override
+ public int deleteEverything() {
+ final SupportSQLiteStatement _stmt = __preparedStmtOfDeleteEverything.acquire();
+ __db.beginTransaction();
+ try {
+ final int _result = _stmt.executeUpdateDelete();
+ __db.setTransactionSuccessful();
+ return _result;
+ } finally {
+ __db.endTransaction();
+ __preparedStmtOfDeleteEverything.release(_stmt);
+ }
+ }
- @Query("DELETE FROM user")
- int deleteEverything();
+ @Override
+ public int deleteByUidList(int... uid) {
+ StringBuilder _stringBuilder = StringUtil.newStringBuilder();
+ _stringBuilder.append("DELETE FROM user where uid IN(");
+ final int _inputSize = uid.length;
+ StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
+ _stringBuilder.append(")");
+ final String _sql = _stringBuilder.toString();
+ SupportSQLiteStatement _stmt = __db.compileStatement(_sql);
+ int _argIndex = 1;
+ for (int _item : uid) {
+ _stmt.bindLong(_argIndex, _item);
+ _argIndex ++;
+ }
+ __db.beginTransaction();
+ try {
+ final int _result = _stmt.executeUpdateDelete();
+ __db.setTransactionSuccessful();
+ return _result;
+ } finally {
+ __db.endTransaction();
+ }
+ }
}
diff --git a/foo/bar/UpdateDao.java b/foo/bar/UpdateDao.java
index 040b5c79..1190a0df 100644
--- a/foo/bar/UpdateDao.java
+++ b/foo/bar/UpdateDao.java
@@ -1,48 +1,240 @@
-/*
- * 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 foo.bar;
-import android.arch.persistence.room.*;
+
+import android.arch.persistence.db.SupportSQLiteStatement;
+import android.arch.persistence.room.EntityDeletionOrUpdateAdapter;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.SharedSQLiteStatement;
+import java.lang.Override;
+import java.lang.String;
import java.util.List;
+import javax.annotation.Generated;
+
+@Generated("android.arch.persistence.room.RoomProcessor")
+public class UpdateDao_Impl implements UpdateDao {
+ private final RoomDatabase __db;
+
+ private final EntityDeletionOrUpdateAdapter __updateAdapterOfUser;
+
+ private final EntityDeletionOrUpdateAdapter __updateAdapterOfMultiPKeyEntity;
+
+ private final EntityDeletionOrUpdateAdapter __updateAdapterOfBook;
+
+ private final SharedSQLiteStatement __preparedStmtOfAgeUserByUid;
+
+ private final SharedSQLiteStatement __preparedStmtOfAgeUserAll;
+
+ public UpdateDao_Impl(RoomDatabase __db) {
+ this.__db = __db;
+ this.__updateAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
+ @Override
+ public String createQuery() {
+ return "UPDATE OR ABORT `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
+ }
+
+ @Override
+ public void bind(SupportSQLiteStatement stmt, User value) {
+ stmt.bindLong(1, value.uid);
+ if (value.name == null) {
+ stmt.bindNull(2);
+ } else {
+ stmt.bindString(2, value.name);
+ }
+ if (value.getLastName() == null) {
+ stmt.bindNull(3);
+ } else {
+ stmt.bindString(3, value.getLastName());
+ }
+ stmt.bindLong(4, value.age);
+ stmt.bindLong(5, value.uid);
+ }
+ };
+ this.__updateAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
+ @Override
+ public String createQuery() {
+ return "UPDATE OR ABORT `MultiPKeyEntity` SET `name` = ?,`lastName` = ? WHERE `name` = ? AND `lastName` = ?";
+ }
+
+ @Override
+ public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
+ if (value.name == null) {
+ stmt.bindNull(1);
+ } else {
+ stmt.bindString(1, value.name);
+ }
+ if (value.lastName == null) {
+ stmt.bindNull(2);
+ } else {
+ stmt.bindString(2, value.lastName);
+ }
+ if (value.name == null) {
+ stmt.bindNull(3);
+ } else {
+ stmt.bindString(3, value.name);
+ }
+ if (value.lastName == null) {
+ stmt.bindNull(4);
+ } else {
+ stmt.bindString(4, value.lastName);
+ }
+ }
+ };
+ this.__updateAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
+ @Override
+ public String createQuery() {
+ return "UPDATE OR ABORT `Book` SET `bookId` = ?,`uid` = ? WHERE `bookId` = ?";
+ }
+
+ @Override
+ public void bind(SupportSQLiteStatement stmt, Book value) {
+ stmt.bindLong(1, value.bookId);
+ stmt.bindLong(2, value.uid);
+ stmt.bindLong(3, value.bookId);
+ }
+ };
+ this.__preparedStmtOfAgeUserByUid = new SharedSQLiteStatement(__db) {
+ @Override
+ public String createQuery() {
+ final String _query = "UPDATE User SET ageColumn = ageColumn + 1 WHERE uid = ?";
+ return _query;
+ }
+ };
+ this.__preparedStmtOfAgeUserAll = new SharedSQLiteStatement(__db) {
+ @Override
+ public String createQuery() {
+ final String _query = "UPDATE User SET ageColumn = ageColumn + 1";
+ return _query;
+ }
+ };
+ }
+
+ @Override
+ public void updateUser(User user) {
+ __db.beginTransaction();
+ try {
+ __updateAdapterOfUser.handle(user);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public void updateUsers(User user1, List<User> others) {
+ __db.beginTransaction();
+ try {
+ __updateAdapterOfUser.handle(user1);
+ __updateAdapterOfUser.handleMultiple(others);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public void updateArrayOfUsers(User[] users) {
+ __db.beginTransaction();
+ try {
+ __updateAdapterOfUser.handleMultiple(users);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public int updateUserAndReturnCount(User user) {
+ int _total = 0;
+ __db.beginTransaction();
+ try {
+ _total +=__updateAdapterOfUser.handle(user);
+ __db.setTransactionSuccessful();
+ return _total;
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public int updateUserAndReturnCount(User user1, List<User> others) {
+ int _total = 0;
+ __db.beginTransaction();
+ try {
+ _total +=__updateAdapterOfUser.handle(user1);
+ _total +=__updateAdapterOfUser.handleMultiple(others);
+ __db.setTransactionSuccessful();
+ return _total;
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public int updateUserAndReturnCount(User[] users) {
+ int _total = 0;
+ __db.beginTransaction();
+ try {
+ _total +=__updateAdapterOfUser.handleMultiple(users);
+ __db.setTransactionSuccessful();
+ return _total;
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public int multiPKey(MultiPKeyEntity entity) {
+ int _total = 0;
+ __db.beginTransaction();
+ try {
+ _total +=__updateAdapterOfMultiPKeyEntity.handle(entity);
+ __db.setTransactionSuccessful();
+ return _total;
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public void updateUserAndBook(User user, Book book) {
+ __db.beginTransaction();
+ try {
+ __updateAdapterOfUser.handle(user);
+ __updateAdapterOfBook.handle(book);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public void ageUserByUid(String uid) {
+ final SupportSQLiteStatement _stmt = __preparedStmtOfAgeUserByUid.acquire();
+ __db.beginTransaction();
+ try {
+ int _argIndex = 1;
+ if (uid == null) {
+ _stmt.bindNull(_argIndex);
+ } else {
+ _stmt.bindString(_argIndex, uid);
+ }
+ _stmt.executeUpdateDelete();
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ __preparedStmtOfAgeUserByUid.release(_stmt);
+ }
+ }
-@Dao
-abstract interface UpdateDao {
- @Update
- void updateUser(User user);
- @Update
- void updateUsers(User user1, List<User> others);
- @Update
- void updateArrayOfUsers(User[] users);
-
- @Update
- int updateUserAndReturnCount(User user);
- @Update
- int updateUserAndReturnCount(User user1, List<User> others);
- @Update
- int updateUserAndReturnCount(User[] users);
-
- @Update
- int multiPKey(MultiPKeyEntity entity);
-
- @Update
- void updateUserAndBook(User user, Book book);
-
- @Query("UPDATE User SET ageColumn = ageColumn + 1 WHERE uid = :uid")
- void ageUserByUid(String uid);
-
- @Query("UPDATE User SET ageColumn = ageColumn + 1")
- void ageUserAll();
+ @Override
+ public void ageUserAll() {
+ final SupportSQLiteStatement _stmt = __preparedStmtOfAgeUserAll.acquire();
+ __db.beginTransaction();
+ try {
+ _stmt.executeUpdateDelete();
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ __preparedStmtOfAgeUserAll.release(_stmt);
+ }
+ }
}
diff --git a/foo/bar/WriterDao.java b/foo/bar/WriterDao.java
index e122479b..cfad0469 100644
--- a/foo/bar/WriterDao.java
+++ b/foo/bar/WriterDao.java
@@ -15,17 +15,131 @@
*/
package foo.bar;
-import android.arch.persistence.room.*;
+
+import android.arch.persistence.db.SupportSQLiteStatement;
+import android.arch.persistence.room.EntityInsertionAdapter;
+import android.arch.persistence.room.RoomDatabase;
+
+import java.lang.Override;
+import java.lang.String;
import java.util.List;
+import javax.annotation.Generated;
+
+@Generated("android.arch.persistence.room.RoomProcessor")
+public class WriterDao_Impl implements WriterDao {
+ private final RoomDatabase __db;
+
+ private final EntityInsertionAdapter __insertionAdapterOfUser;
+
+ private final EntityInsertionAdapter __insertionAdapterOfUser_1;
+
+ private final EntityInsertionAdapter __insertionAdapterOfBook;
+
+ public WriterDao_Impl(RoomDatabase __db) {
+ this.__db = __db;
+ this.__insertionAdapterOfUser = new EntityInsertionAdapter<User>(__db) {
+ @Override
+ public String createQuery() {
+ return "INSERT OR ABORT INTO `User`(`uid`,`name`,`lastName`,`ageColumn`) VALUES"
+ + " (?,?,?,?)";
+ }
+
+ @Override
+ public void bind(SupportSQLiteStatement stmt, User value) {
+ stmt.bindLong(1, value.uid);
+ if (value.name == null) {
+ stmt.bindNull(2);
+ } else {
+ stmt.bindString(2, value.name);
+ }
+ if (value.getLastName() == null) {
+ stmt.bindNull(3);
+ } else {
+ stmt.bindString(3, value.getLastName());
+ }
+ stmt.bindLong(4, value.age);
+ }
+ };
+ this.__insertionAdapterOfUser_1 = new EntityInsertionAdapter<User>(__db) {
+ @Override
+ public String createQuery() {
+ return "INSERT OR REPLACE INTO `User`(`uid`,`name`,`lastName`,`ageColumn`) VALUES"
+ + " (?,?,?,?)";
+ }
+
+ @Override
+ public void bind(SupportSQLiteStatement stmt, User value) {
+ stmt.bindLong(1, value.uid);
+ if (value.name == null) {
+ stmt.bindNull(2);
+ } else {
+ stmt.bindString(2, value.name);
+ }
+ if (value.getLastName() == null) {
+ stmt.bindNull(3);
+ } else {
+ stmt.bindString(3, value.getLastName());
+ }
+ stmt.bindLong(4, value.age);
+ }
+ };
+ this.__insertionAdapterOfBook = new EntityInsertionAdapter<Book>(__db) {
+ @Override
+ public String createQuery() {
+ return "INSERT OR ABORT INTO `Book`(`bookId`,`uid`) VALUES (?,?)";
+ }
+
+ @Override
+ public void bind(SupportSQLiteStatement stmt, Book value) {
+ stmt.bindLong(1, value.bookId);
+ stmt.bindLong(2, value.uid);
+ }
+ };
+ }
+
+ @Override
+ public void insertUser(User user) {
+ __db.beginTransaction();
+ try {
+ __insertionAdapterOfUser.insert(user);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public void insertUsers(User user1, List<User> others) {
+ __db.beginTransaction();
+ try {
+ __insertionAdapterOfUser.insert(user1);
+ __insertionAdapterOfUser.insert(others);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
+ public void insertUsers(User[] users) {
+ __db.beginTransaction();
+ try {
+ __insertionAdapterOfUser_1.insert(users);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
-@Dao
-abstract interface WriterDao {
- @Insert
- void insertUser(User user);
- @Insert
- void insertUsers(User user1, List<User> others);
- @Insert(onConflict=OnConflictStrategy.REPLACE)
- void insertUsers(User[] users);
- @Insert
- void insertUserAndBook(User user, Book book);
+ @Override
+ public void insertUserAndBook(User user, Book book) {
+ __db.beginTransaction();
+ try {
+ __insertionAdapterOfUser.insert(user);
+ __insertionAdapterOfBook.insert(book);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
}
diff --git a/java/io/CharArrayReader.java b/java/io/CharArrayReader.java
index 2190be39..41409764 100644
--- a/java/io/CharArrayReader.java
+++ b/java/io/CharArrayReader.java
@@ -130,11 +130,12 @@ public class CharArrayReader extends Reader {
if (pos >= count) {
return -1;
}
-
+ // BEGIN Android-changed: Backport of OpenJDK 9b132 fix to avoid integer overflow.
int avail = count - pos;
if (len > avail) {
len = avail;
}
+ // END Android-changed: Backport of OpenJDK 9b132 fix to avoid integer overflow.
if (len <= 0) {
return 0;
}
@@ -159,11 +160,12 @@ public class CharArrayReader extends Reader {
public long skip(long n) throws IOException {
synchronized (lock) {
ensureOpen();
-
+ // BEGIN Android-changed: Backport of OpenJDK 9b132 fix to avoid integer overflow.
long avail = count - pos;
if (n > avail) {
n = avail;
}
+ // END Android-changed: Backport of OpenJDK 9b132 fix to avoid integer overflow.
if (n < 0) {
return 0;
}
diff --git a/java/io/FileInputStream.java b/java/io/FileInputStream.java
index a286aaa5..b44bc5a4 100644
--- a/java/io/FileInputStream.java
+++ b/java/io/FileInputStream.java
@@ -67,9 +67,14 @@ class FileInputStream extends InputStream
private final Object closeLock = new Object();
private volatile boolean closed = false;
+
+ // Android-added: Field for tracking whether the stream owns the underlying FileDescriptor.
private final boolean isFdOwner;
+ // Android-added: CloseGuard support.
private final CloseGuard guard = CloseGuard.get();
+
+ // Android-added: Tracking of unbuffered I/O.
private final IoTracker tracker = new IoTracker();
/**
@@ -143,14 +148,23 @@ class FileInputStream extends InputStream
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
+
+ // Android-changed: Tracking mechanism for FileDescriptor sharing.
+ // fd.attach(this);
isFdOwner = true;
- this.path = name;
+ path = name;
+
+ // Android-added: BlockGuard support.
BlockGuard.getThreadPolicy().onReadFromDisk();
+
open(name);
+
+ // Android-added: CloseGuard support.
guard.open("close");
}
+ // Android-removed: Documentation around SecurityException. Not thrown on Android.
/**
* Creates a <code>FileInputStream</code> by using the file descriptor
* <code>fdObj</code>, which represents an existing connection to an
@@ -172,17 +186,30 @@ class FileInputStream extends InputStream
* @param fdObj the file descriptor to be opened for reading.
*/
public FileInputStream(FileDescriptor fdObj) {
+ // Android-changed: Delegate to added hidden constructor.
this(fdObj, false /* isFdOwner */);
}
+ // Android-added: Internal/hidden constructor for specifying FileDescriptor ownership.
+ // Android-removed: SecurityManager calls.
/** @hide */
public FileInputStream(FileDescriptor fdObj, boolean isFdOwner) {
if (fdObj == null) {
+ // Android-changed: Improved NullPointerException message.
throw new NullPointerException("fdObj == null");
}
fd = fdObj;
- this.isFdOwner = isFdOwner;
path = null;
+
+ // Android-changed: FileDescriptor ownership tracking mechanism.
+ /*
+ /*
+ * FileDescriptor is being shared by streams.
+ * Register this stream with FileDescriptor tracker.
+ *
+ fd.attach(this);
+ */
+ this.isFdOwner = isFdOwner;
}
/**
@@ -209,10 +236,26 @@ class FileInputStream extends InputStream
* @exception IOException if an I/O error occurs.
*/
public int read() throws IOException {
+ // Android-changed: Read methods delegate to read(byte[], int, int) to share Android logic.
byte[] b = new byte[1];
return (read(b, 0, 1) != -1) ? b[0] & 0xff : -1;
}
+ // Android-removed: Read methods delegate to read(byte[], int, int) to share Android logic.
+ // private native int read0() throws IOException;
+
+ // Android-removed: Read methods delegate to read(byte[], int, int) to share Android logic.
+ /*
+ /**
+ * Reads a subarray as a sequence of bytes.
+ * @param b the data to be written
+ * @param off the start offset in the data
+ * @param len the number of bytes that are written
+ * @exception IOException If an I/O error has occurred.
+ *
+ private native int readBytes(byte b[], int off, int len) throws IOException;
+ */
+
/**
* Reads up to <code>b.length</code> bytes of data from this input
* stream into an array of bytes. This method blocks until some input
@@ -225,6 +268,7 @@ class FileInputStream extends InputStream
* @exception IOException if an I/O error occurs.
*/
public int read(byte b[]) throws IOException {
+ // Android-changed: Read methods delegate to read(byte[], int, int) to share Android logic.
return read(b, 0, b.length);
}
@@ -247,10 +291,15 @@ class FileInputStream extends InputStream
* @exception IOException if an I/O error occurs.
*/
public int read(byte b[], int off, int len) throws IOException {
+ // Android-added: close() check before I/O.
if (closed && len > 0) {
throw new IOException("Stream Closed");
}
+
+ // Android-added: Tracking of unbuffered I/O.
tracker.trackIo(len);
+
+ // Android-changed: Use IoBridge instead of calling native method.
return IoBridge.read(fd, b, off, len);
}
@@ -278,12 +327,15 @@ class FileInputStream extends InputStream
* @exception IOException if n is negative, if the stream does not
* support seek, or if an I/O error occurs.
*/
+ // BEGIN Android-changed: skip(long) implementation changed from bare native.
public long skip(long n) throws IOException {
+ // Android-added: close() check before I/O.
if (closed) {
throw new IOException("Stream Closed");
}
try {
+ // Android-added: BlockGuard support.
BlockGuard.getThreadPolicy().onReadFromDisk();
return skip0(n);
} catch(UseManualSkipException e) {
@@ -298,6 +350,7 @@ class FileInputStream extends InputStream
*/
private static class UseManualSkipException extends Exception {
}
+ // END Android-changed: skip(long) implementation changed from bare native.
/**
* Returns an estimate of the number of remaining bytes that can be read (or
@@ -316,7 +369,9 @@ class FileInputStream extends InputStream
* @exception IOException if this file input stream has been closed by calling
* {@code close} or an I/O error occurs.
*/
+ // BEGIN Android-changed: available() implementation changed from bare native.
public int available() throws IOException {
+ // Android-added: close() check before I/O.
if (closed) {
throw new IOException("Stream Closed");
}
@@ -325,6 +380,7 @@ class FileInputStream extends InputStream
}
private native int available0() throws IOException;
+ // END Android-changed: available() implementation changed from bare native.
/**
* Closes this file input stream and releases any system resources
@@ -346,20 +402,18 @@ class FileInputStream extends InputStream
closed = true;
}
+ // Android-added: CloseGuard support.
guard.close();
if (channel != null) {
- /*
- * Decrement the FD use count associated with the channel
- * The use count is incremented whenever a new channel
- * is obtained from this stream.
- */
channel.close();
}
+ // BEGIN Android-changed: Close handling / notification of blocked threads.
if (isFdOwner) {
IoBridge.closeAndSignalBlockedThreads(fd);
}
+ // END Android-changed: Close handling / notification of blocked threads.
}
/**
@@ -399,12 +453,23 @@ class FileInputStream extends InputStream
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, false, this);
-
}
return channel;
}
}
+ // BEGIN Android-removed: Unused code.
+ /*
+ private static native void initIDs();
+
+ private native void close0() throws IOException;
+
+ static {
+ initIDs();
+ }
+ */
+ // END Android-changed: Unused code.
+
/**
* Ensures that the <code>close</code> method of this file input stream is
* called when there are no more references to it.
@@ -413,11 +478,13 @@ class FileInputStream extends InputStream
* @see java.io.FileInputStream#close()
*/
protected void finalize() throws IOException {
+ // Android-added: CloseGuard support.
if (guard != null) {
guard.warnIfOpen();
}
if ((fd != null) && (fd != FileDescriptor.in)) {
+ // Android-removed: Obsoleted comment about shared FileDescriptor handling.
close();
}
}
diff --git a/java/io/FileOutputStream.java b/java/io/FileOutputStream.java
index 249e337a..79e76900 100644
--- a/java/io/FileOutputStream.java
+++ b/java/io/FileOutputStream.java
@@ -72,17 +72,22 @@ class FileOutputStream extends OutputStream
*/
private FileChannel channel;
- private final Object closeLock = new Object();
- private volatile boolean closed = false;
-
/**
* The path of the referenced file
* (null if the stream is created with a file descriptor)
*/
private final String path;
+ private final Object closeLock = new Object();
+ private volatile boolean closed = false;
+
+ // Android-added: CloseGuard support: Log if the stream is not closed.
private final CloseGuard guard = CloseGuard.get();
+
+ // Android-added: Field for tracking whether the stream owns the underlying FileDescriptor.
private final boolean isFdOwner;
+
+ // Android-added: Tracking of unbuffered I/O.
private final IoTracker tracker = new IoTracker();
/**
@@ -215,15 +220,24 @@ class FileOutputStream extends OutputStream
throw new FileNotFoundException("Invalid file path");
}
this.fd = new FileDescriptor();
+
+ // Android-changed: Tracking mechanism for FileDescriptor sharing.
+ // fd.attach(this);
+ this.isFdOwner = true;
+
this.append = append;
this.path = name;
- this.isFdOwner = true;
+ // Android-added: BlockGuard support.
BlockGuard.getThreadPolicy().onWriteToDisk();
+
open(name, append);
+
+ // Android-added: CloseGuard support.
guard.open("close");
}
+ // Android-removed: Documentation around SecurityException. Not thrown on Android.
/**
* Creates a file output stream to write to the specified file
* descriptor, which represents an existing connection to an actual
@@ -242,15 +256,14 @@ class FileOutputStream extends OutputStream
* I/O on the stream, an <code>IOException</code> is thrown.
*
* @param fdObj the file descriptor to be opened for writing
- * @exception SecurityException if a security manager exists and its
- * <code>checkWrite</code> method denies
- * write access to the file descriptor
- * @see java.lang.SecurityManager#checkWrite(java.io.FileDescriptor)
*/
public FileOutputStream(FileDescriptor fdObj) {
+ // Android-changed: Delegate to added hidden constructor.
this(fdObj, false /* isOwner */);
}
+ // Android-added: Internal/hidden constructor for specifying FileDescriptor ownership.
+ // Android-removed: SecurityManager calls.
/**
* Internal constructor for {@code FileOutputStream} objects where the file descriptor
* is owned by this tream.
@@ -259,12 +272,16 @@ class FileOutputStream extends OutputStream
*/
public FileOutputStream(FileDescriptor fdObj, boolean isFdOwner) {
if (fdObj == null) {
+ // Android-changed: Improved NullPointerException message.
throw new NullPointerException("fdObj == null");
}
this.fd = fdObj;
- this.path = null;
this.append = false;
+ this.path = null;
+
+ // Android-changed: FileDescriptor ownership tracking mechanism.
+ // fd.attach(this);
this.isFdOwner = isFdOwner;
}
@@ -287,6 +304,18 @@ class FileOutputStream extends OutputStream
open0(name, append);
}
+ // Android-removed: write(int, boolean), use IoBridge instead.
+ /*
+ /**
+ * Writes the specified byte to this file output stream.
+ *
+ * @param b the byte to be written.
+ * @param append {@code true} if the write operation first
+ * advances the position to the end of file
+ *
+ private native void write(int b, boolean append) throws IOException;
+ */
+
/**
* Writes the specified byte to this file output stream. Implements
* the <code>write</code> method of <code>OutputStream</code>.
@@ -295,9 +324,25 @@ class FileOutputStream extends OutputStream
* @exception IOException if an I/O error occurs.
*/
public void write(int b) throws IOException {
+ // Android-changed: Write methods delegate to write(byte[],int,int) to share Android logic.
write(new byte[] { (byte) b }, 0, 1);
}
+ // Android-removed: Write methods delegate to write(byte[],int,int) to share Android logic.
+ /*
+ /**
+ * Writes a sub array as a sequence of bytes.
+ * @param b the data to be written
+ * @param off the start offset in the data
+ * @param len the number of bytes that are written
+ * @param append {@code true} to first advance the position to the
+ * end of file
+ * @exception IOException If an I/O error has occurred.
+ *
+ private native void writeBytes(byte b[], int off, int len, boolean append)
+ throws IOException;
+ */
+
/**
* Writes <code>b.length</code> bytes from the specified byte array
* to this file output stream.
@@ -306,6 +351,7 @@ class FileOutputStream extends OutputStream
* @exception IOException if an I/O error occurs.
*/
public void write(byte b[]) throws IOException {
+ // Android-changed: Write methods delegate to write(byte[],int,int) to share Android logic.
write(b, 0, b.length);
}
@@ -319,10 +365,15 @@ class FileOutputStream extends OutputStream
* @exception IOException if an I/O error occurs.
*/
public void write(byte b[], int off, int len) throws IOException {
+ // Android-added: close() check before I/O.
if (closed && len > 0) {
throw new IOException("Stream Closed");
}
+
+ // Android-added: Tracking of unbuffered I/O.
tracker.trackIo(len);
+
+ // Android-changed: Use IoBridge instead of calling native method.
IoBridge.write(fd, b, off, len);
}
@@ -347,21 +398,18 @@ class FileOutputStream extends OutputStream
closed = true;
}
+ // Android-added: CloseGuard support.
guard.close();
if (channel != null) {
- /*
- * Decrement FD use count associated with the channel
- * The use count is incremented whenever a new channel
- * is obtained from this stream.
- */
channel.close();
}
-
+ // BEGIN Android-changed: Close handling / notification of blocked threads.
if (isFdOwner) {
IoBridge.closeAndSignalBlockedThreads(fd);
}
+ // END Android-changed: Close handling / notification of blocked threads.
}
/**
@@ -416,6 +464,7 @@ class FileOutputStream extends OutputStream
* @see java.io.FileInputStream#close()
*/
protected void finalize() throws IOException {
+ // Android-added: CloseGuard support.
if (guard != null) {
guard.warnIfOpen();
}
@@ -424,8 +473,22 @@ class FileOutputStream extends OutputStream
if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
flush();
} else {
+ // Android-removed: Obsoleted comment about shared FileDescriptor handling.
close();
}
}
}
+
+ // BEGIN Android-removed: Unused code.
+ /*
+ private native void close0() throws IOException;
+
+ private static native void initIDs();
+
+ static {
+ initIDs();
+ }
+ */
+ // END Android-removed: Unused code.
+
}
diff --git a/java/io/FileSystem.java b/java/io/FileSystem.java
index 86d8fff2..ffa640c2 100644
--- a/java/io/FileSystem.java
+++ b/java/io/FileSystem.java
@@ -115,6 +115,7 @@ abstract class FileSystem {
@Native public static final int ACCESS_READ = 0x04;
@Native public static final int ACCESS_WRITE = 0x02;
@Native public static final int ACCESS_EXECUTE = 0x01;
+ // Android-added: b/25878034, to support File.exists() reimplementation.
public static final int ACCESS_OK = 0x08;
/**
diff --git a/java/io/StringBufferInputStream.java b/java/io/StringBufferInputStream.java
index f47eebcd..90c5d4d7 100644
--- a/java/io/StringBufferInputStream.java
+++ b/java/io/StringBufferInputStream.java
@@ -118,11 +118,12 @@ class StringBufferInputStream extends InputStream {
if (pos >= count) {
return -1;
}
-
+ // BEGIN Android-changed: Backport of OpenJDK 9b132 fix to avoid integer overflow.
int avail = count - pos;
if (len > avail) {
len = avail;
}
+ // END Android-changed: Backport of OpenJDK 9b132 fix to avoid integer overflow.
if (len <= 0) {
return 0;
}
diff --git a/java/lang/Iterable.java b/java/lang/Iterable.java
index b897d073..10ff0bbe 100644
--- a/java/lang/Iterable.java
+++ b/java/lang/Iterable.java
@@ -30,6 +30,7 @@ import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
+// Android-changed: href changed in javadoc for redirection.
/**
* Implementing this interface allows an object to be the target of
* the "for-each loop" statement. See
diff --git a/java/lang/reflect/AccessibleObject.java b/java/lang/reflect/AccessibleObject.java
index 79413767..18442fac 100644
--- a/java/lang/reflect/AccessibleObject.java
+++ b/java/lang/reflect/AccessibleObject.java
@@ -183,7 +183,8 @@ public class AccessibleObject implements AnnotatedElement {
// outside this package.
boolean override;
- /* Android-removed: reflectionFactory: it is not used on Android.
+ // Android-removed: reflectionFactory: it is not used on Android.
+ /*
// Reflection factory used by subclasses for creating field,
// method, and constructor accessors. Note that this is called
// very early in the bootstrapping process.
diff --git a/java/lang/reflect/Constructor.java b/java/lang/reflect/Constructor.java
index de7176bb..cf6b29ff 100644
--- a/java/lang/reflect/Constructor.java
+++ b/java/lang/reflect/Constructor.java
@@ -29,6 +29,7 @@ package java.lang.reflect;
import dalvik.annotation.optimization.FastNative;
import libcore.util.EmptyArray;
+import sun.reflect.CallerSensitive;
import java.lang.annotation.Annotation;
import java.util.Comparator;
@@ -53,6 +54,11 @@ import java.util.Comparator;
* @author Nakul Saraiya
*/
public final class Constructor<T> extends Executable {
+ // Android-changed: Extensive modifications made throughout the class for ART.
+ // Android-changed: Many fields and methods removed / modified.
+ // Android-removed: Type annotations runtime code. Not supported on Android.
+ // Android-removed: Declared vs actual parameter annotation indexes handling.
+
private static final Comparator<Method> ORDER_BY_SIGNATURE = null; // Unused; must match Method.
private final Class<?> serializationClass;
@@ -77,7 +83,7 @@ public final class Constructor<T> extends Executable {
@Override
boolean hasGenericInformation() {
- // Android-changed: Signature retrieval is handled in Executable.
+ // Android-changed: hasGenericInformation() implemented using Executable.
return super.hasGenericInformationInternal();
}
@@ -85,9 +91,9 @@ public final class Constructor<T> extends Executable {
* {@inheritDoc}
*/
@Override
+ // Android-changed: getDeclaringClass() implemented using Executable.
@SuppressWarnings({"rawtypes", "unchecked"})
public Class<T> getDeclaringClass() {
- // Android-changed: This is handled by Executable.
return (Class<T>) super.getDeclaringClassInternal();
}
@@ -105,7 +111,7 @@ public final class Constructor<T> extends Executable {
*/
@Override
public int getModifiers() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getModifiers() implemented using Executable.
return super.getModifiersInternal();
}
@@ -117,7 +123,7 @@ public final class Constructor<T> extends Executable {
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public TypeVariable<Constructor<T>>[] getTypeParameters() {
- // Android-changed: This is mostly handled by Executable.
+ // Android-changed: getTypeParameters() partly implemented using Executable.
GenericInfo info = getMethodOrConstructorGenericInfoInternal();
return (TypeVariable<Constructor<T>>[]) info.formalTypeParameters.clone();
}
@@ -128,7 +134,7 @@ public final class Constructor<T> extends Executable {
*/
@Override
public Class<?>[] getParameterTypes() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getParameterTypes() partly implemented using Executable.
Class<?>[] paramTypes = super.getParameterTypesInternal();
if (paramTypes == null) {
return EmptyArray.CLASS;
@@ -142,7 +148,7 @@ public final class Constructor<T> extends Executable {
* @since 1.8
*/
public int getParameterCount() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getParameterCount() implemented using Executable.
return super.getParameterCountInternal();
}
@@ -162,6 +168,7 @@ public final class Constructor<T> extends Executable {
* {@inheritDoc}
*/
@Override
+ // Android-changed: getExceptionTypes() implemented in native code.
@FastNative
public native Class<?>[] getExceptionTypes();
@@ -187,7 +194,7 @@ public final class Constructor<T> extends Executable {
if (obj != null && obj instanceof Constructor) {
Constructor<?> other = (Constructor<?>)obj;
if (getDeclaringClass() == other.getDeclaringClass()) {
- // Android-changed: Use getParameterTypes.
+ // Android-changed: Use getParameterTypes() instead of deleted parameterTypes field
return equalParamTypes(getParameterTypes(), other.getParameterTypes());
}
}
@@ -222,7 +229,7 @@ public final class Constructor<T> extends Executable {
* @jls 8.8.3. Constructor Modifiers
*/
public String toString() {
- // Android-changed: Use getParameterTypes().
+ // Android-changed: Use getParameterTypes() / getExceptionTypes() instead of deleted fields
return sharedToString(Modifier.constructorModifiers(),
false,
getParameterTypes(),
@@ -326,6 +333,8 @@ public final class Constructor<T> extends Executable {
* @exception ExceptionInInitializerError if the initialization provoked
* by this method fails.
*/
+ // BEGIN Android-changed: newInstance(Object...) implemented differently.
+ @CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
@@ -344,6 +353,7 @@ public final class Constructor<T> extends Executable {
@FastNative
private native T newInstance0(Object... args) throws InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException;
+ // END Android-changed: newInstance(Object...) implemented differently.
/**
* {@inheritDoc}
@@ -387,7 +397,7 @@ public final class Constructor<T> extends Executable {
*/
@Override
public Annotation[][] getParameterAnnotations() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getParameterAnnotations() implemented using Executable.
return super.getParameterAnnotationsInternal();
}
}
diff --git a/java/lang/reflect/Executable.java b/java/lang/reflect/Executable.java
index 7945ad61..6782d00e 100644
--- a/java/lang/reflect/Executable.java
+++ b/java/lang/reflect/Executable.java
@@ -43,6 +43,11 @@ import libcore.util.EmptyArray;
*/
public abstract class Executable extends AccessibleObject
implements Member, GenericDeclaration {
+
+ // Android-changed: Extensive modifications made throughout the class for ART.
+ // Android-removed: Declared vs actual parameter annotation indexes handling.
+ // Android-removed: Type annotations runtime code. Not supported on Android.
+
/*
* Only grant package-visibility to the constructor.
*/
@@ -226,7 +231,6 @@ public abstract class Executable extends AccessibleObject
* declared or implicitly declared or neither) for the executable
* represented by this object.
*
- * @since 1.8
* @return The number of formal parameters for the executable this
* object represents
*/
@@ -261,7 +265,7 @@ public abstract class Executable extends AccessibleObject
* type that cannot be instantiated for any reason
*/
public Type[] getGenericParameterTypes() {
- // Android-changed: Changed to use Types / getMethodOrConstructorGenericInfoInternal()
+ // Android-changed: getGenericParameterTypes() implementation for use with ART.
return Types.getTypeArray(
getMethodOrConstructorGenericInfoInternal().genericParameterTypes, false);
}
@@ -323,7 +327,6 @@ public abstract class Executable extends AccessibleObject
* have unique names, or names that are legal identifiers in the
* Java programming language (JLS 3.8).
*
- * @since 1.8
* @throws MalformedParametersException if the class file contains
* a MethodParameters attribute that is improperly formatted.
* @return an array of {@code Parameter} objects representing all
@@ -380,6 +383,7 @@ public abstract class Executable extends AccessibleObject
Parameter[] tmp = parameters;
if (tmp == null) {
+
// Otherwise, go to the JVM to get them
try {
tmp = getParameters0();
@@ -420,6 +424,7 @@ public abstract class Executable extends AccessibleObject
private transient volatile boolean hasRealParameterData;
private transient volatile Parameter[] parameters;
+ // Android-changed: Added @FastNative to getParameters0()
@FastNative
private native Parameter[] getParameters0();
@@ -457,7 +462,7 @@ public abstract class Executable extends AccessibleObject
* parameterized type that cannot be instantiated for any reason
*/
public Type[] getGenericExceptionTypes() {
- // Android-changed: Changed to use Types / getMethodOrConstructorGenericInfoInternal()
+ // Android-changed: getGenericExceptionTypes() implementation for use with ART.
return Types.getTypeArray(
getMethodOrConstructorGenericInfoInternal().genericExceptionTypes, false);
}
@@ -478,7 +483,7 @@ public abstract class Executable extends AccessibleObject
* to take a variable number of arguments.
*/
public boolean isVarArgs() {
- // Android-changed: Slightly more efficient.
+ // Android-changed: isVarArgs() made slightly more efficient.
return (accessFlags & Modifier.VARARGS) != 0;
}
@@ -492,7 +497,7 @@ public abstract class Executable extends AccessibleObject
* @jls 13.1 The Form of a Binary
*/
public boolean isSynthetic() {
- // Android-changed: Slightly more efficient.
+ // Android-changed: isSynthetic() made slightly more efficient.
return (accessFlags & Modifier.SYNTHETIC) != 0;
}
@@ -532,20 +537,21 @@ public abstract class Executable extends AccessibleObject
*/
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
Objects.requireNonNull(annotationClass);
- // Android-changed: Implemented natively.
+ // Android-changed: Implemented getAnnotation(Class) natively.
return getAnnotationNative(annotationClass);
}
+
+ // Android-changed: Implemented getAnnotation(Class) natively.
@FastNative
private native <T extends Annotation> T getAnnotationNative(Class<T> annotationClass);
/**
* {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
- * @since 1.8
*/
@Override
public <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
- // Android-changed: Uses AnnotatedElements instead.
+ // Android-changed: getAnnotationsByType(Class), Android uses AnnotatedElements instead.
return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
}
@@ -553,14 +559,16 @@ public abstract class Executable extends AccessibleObject
* {@inheritDoc}
*/
public Annotation[] getDeclaredAnnotations() {
- // Android-changed: Implemented natively.
+ // Android-changed: Implemented getDeclaredAnnotations() natively.
return getDeclaredAnnotationsNative();
}
+
+ // Android-added: Implemented getDeclaredAnnotations() natively.
@FastNative
private native Annotation[] getDeclaredAnnotationsNative();
- // Android-changed: Additional ART-related fields and logic below that is shared between
- // Method and Constructor.
+ // BEGIN Android-added: Additional ART-related fields and logic.
+ // This code is shared for Method and Constructor.
/** Bits encoding access (e.g. public, private) as well as other runtime specific flags */
@SuppressWarnings("unused") // set by runtime
@@ -737,4 +745,6 @@ public abstract class Executable extends AccessibleObject
final boolean isBridgeMethodInternal() {
return (accessFlags & Modifier.BRIDGE) != 0;
}
+ // END Android-added: Additional ART-related fields and logic.
+
}
diff --git a/java/lang/reflect/Field.java b/java/lang/reflect/Field.java
index a6917660..2bcf9e92 100644
--- a/java/lang/reflect/Field.java
+++ b/java/lang/reflect/Field.java
@@ -27,7 +27,7 @@
package java.lang.reflect;
import dalvik.annotation.optimization.FastNative;
-
+import sun.reflect.CallerSensitive;
import java.lang.annotation.Annotation;
import java.util.Objects;
import libcore.reflect.AnnotatedElements;
@@ -55,6 +55,9 @@ import libcore.reflect.GenericSignatureParser;
*/
public final
class Field extends AccessibleObject implements Member {
+ // Android-changed: Extensive modifications made throughout the class for ART.
+ // Android-changed: Many fields and methods removed / modified.
+ // Android-removed: Type annotations runtime code. Not supported on Android.
private int accessFlags;
private Class<?> declaringClass;
@@ -70,6 +73,7 @@ class Field extends AccessibleObject implements Member {
* that declares the field represented by this {@code Field} object.
*/
public Class<?> getDeclaringClass() {
+ // Android-changed: Adjust code for different field names.
return declaringClass;
}
@@ -77,6 +81,7 @@ class Field extends AccessibleObject implements Member {
* Returns the name of the field represented by this {@code Field} object.
*/
public String getName() {
+ // Android-changed: getName() implemented differently.
if (dexFieldIndex == -1) {
// Proxy classes have 1 synthesized static field with no valid dex index.
if (!declaringClass.isProxy()) {
@@ -88,6 +93,7 @@ class Field extends AccessibleObject implements Member {
return getNameInternal();
}
+ // Android-added: getName() implemented differently.
@FastNative
private native String getNameInternal();
@@ -99,6 +105,7 @@ class Field extends AccessibleObject implements Member {
* @see Modifier
*/
public int getModifiers() {
+ // Android-changed: Adjust getModifiers() implementation to mask extra bits used on Android.
return accessFlags & 0xffff; // mask out bits not used by Java
}
@@ -163,6 +170,7 @@ class Field extends AccessibleObject implements Member {
* @since 1.5
*/
public Type getGenericType() {
+ // Android-changed: getGenericType() implemented differently.
String signatureAttribute = getSignatureAttribute();
ClassLoader cl = declaringClass.getClassLoader();
GenericSignatureParser parser = new GenericSignatureParser(cl);
@@ -174,6 +182,7 @@ class Field extends AccessibleObject implements Member {
return genericType;
}
+ // BEGIN Android-added: getGenericType() implemented differently.
private String getSignatureAttribute() {
String[] annotation = getSignatureAnnotation();
if (annotation == null) {
@@ -187,6 +196,7 @@ class Field extends AccessibleObject implements Member {
}
@FastNative
private native String[] getSignatureAnnotation();
+ // END Android-added: getGenericType() implemented differently.
/**
@@ -319,6 +329,8 @@ class Field extends AccessibleObject implements Member {
* @exception ExceptionInInitializerError if the initialization provoked
* by this method fails.
*/
+ @CallerSensitive
+ // Android-changed: get*(Object) implemented natively.
@FastNative
public native Object get(Object obj)
throws IllegalArgumentException, IllegalAccessException;
@@ -345,6 +357,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#get
*/
+ @CallerSensitive
+ // Android-changed: get*(Object) implemented natively.
@FastNative
public native boolean getBoolean(Object obj)
throws IllegalArgumentException, IllegalAccessException;
@@ -371,6 +385,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#get
*/
+ @CallerSensitive
+ // Android-changed: get*(Object) implemented natively.
@FastNative
public native byte getByte(Object obj)
throws IllegalArgumentException, IllegalAccessException;
@@ -399,6 +415,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#get
*/
+ @CallerSensitive
+ // Android-changed: get*(Object) implemented natively.
@FastNative
public native char getChar(Object obj)
throws IllegalArgumentException, IllegalAccessException;
@@ -427,6 +445,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#get
*/
+ @CallerSensitive
+ // Android-changed: get*(Object) implemented natively.
@FastNative
public native short getShort(Object obj)
throws IllegalArgumentException, IllegalAccessException;
@@ -455,6 +475,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#get
*/
+ @CallerSensitive
+ // Android-changed: get*(Object) implemented natively.
@FastNative
public native int getInt(Object obj)
throws IllegalArgumentException, IllegalAccessException;
@@ -483,6 +505,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#get
*/
+ @CallerSensitive
+ // Android-changed: get*(Object) implemented natively.
@FastNative
public native long getLong(Object obj)
throws IllegalArgumentException, IllegalAccessException;
@@ -511,6 +535,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#get
*/
+ @CallerSensitive
+ // Android-changed: get*(Object) implemented natively.
@FastNative
public native float getFloat(Object obj)
throws IllegalArgumentException, IllegalAccessException;
@@ -539,6 +565,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#get
*/
+ @CallerSensitive
+ // Android-changed: get*(Object) implemented natively.
@FastNative
public native double getDouble(Object obj)
throws IllegalArgumentException, IllegalAccessException;
@@ -609,6 +637,8 @@ class Field extends AccessibleObject implements Member {
* @exception ExceptionInInitializerError if the initialization provoked
* by this method fails.
*/
+ @CallerSensitive
+ // Android-changed: set*(Object, ...) implemented natively.
@FastNative
public native void set(Object obj, Object value)
throws IllegalArgumentException, IllegalAccessException;
@@ -637,6 +667,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#set
*/
+ @CallerSensitive
+ // Android-changed: set*(Object, ...) implemented natively.
@FastNative
public native void setBoolean(Object obj, boolean z)
throws IllegalArgumentException, IllegalAccessException;
@@ -665,6 +697,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#set
*/
+ @CallerSensitive
+ // Android-changed: set*(Object, ...) implemented natively.
@FastNative
public native void setByte(Object obj, byte b)
throws IllegalArgumentException, IllegalAccessException;
@@ -693,6 +727,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#set
*/
+ @CallerSensitive
+ // Android-changed: set*(Object, ...) implemented natively.
@FastNative
public native void setChar(Object obj, char c)
throws IllegalArgumentException, IllegalAccessException;
@@ -721,6 +757,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#set
*/
+ @CallerSensitive
+ // Android-changed: set*(Object, ...) implemented natively.
@FastNative
public native void setShort(Object obj, short s)
throws IllegalArgumentException, IllegalAccessException;
@@ -749,6 +787,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#set
*/
+ @CallerSensitive
+ // Android-changed: set*(Object, ...) implemented natively.
@FastNative
public native void setInt(Object obj, int i)
throws IllegalArgumentException, IllegalAccessException;
@@ -777,6 +817,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#set
*/
+ @CallerSensitive
+ // Android-changed: set*(Object, ...) implemented natively.
@FastNative
public native void setLong(Object obj, long l)
throws IllegalArgumentException, IllegalAccessException;
@@ -805,6 +847,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#set
*/
+ @CallerSensitive
+ // Android-changed: set*(Object, ...) implemented natively.
@FastNative
public native void setFloat(Object obj, float f)
throws IllegalArgumentException, IllegalAccessException;
@@ -833,6 +877,8 @@ class Field extends AccessibleObject implements Member {
* by this method fails.
* @see Field#set
*/
+ @CallerSensitive
+ // Android-changed: set*(Object, ...) implemented natively.
@FastNative
public native void setDouble(Object obj, double d)
throws IllegalArgumentException, IllegalAccessException;
@@ -841,11 +887,12 @@ class Field extends AccessibleObject implements Member {
* @throws NullPointerException {@inheritDoc}
* @since 1.5
*/
- @Override
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
Objects.requireNonNull(annotationClass);
+ // Android-changed: getAnnotation(Class) implemented differently.
return getAnnotationNative(annotationClass);
}
+ // Android-added: getAnnotation(Class) implemented differently.
@FastNative
private native <A extends Annotation> A getAnnotationNative(Class<A> annotationType);
@@ -856,12 +903,13 @@ class Field extends AccessibleObject implements Member {
*/
@Override
public <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
- // Android-changed: Uses AnnotatedElements instead.
+ // Android-added: getAnnotationsByType(Class) implemented differently.
return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
}
-
- @Override public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
+ // BEGIN Android-added: isAnnotationPresent(Class) overridden in Field.
+ @Override
+ public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
if (annotationType == null) {
throw new NullPointerException("annotationType == null");
}
@@ -869,6 +917,7 @@ class Field extends AccessibleObject implements Member {
}
@FastNative
private native boolean isAnnotationPresentNative(Class<? extends Annotation> annotationType);
+ // END Android-added: isAnnotationPresent(Class) overridden in Field.
/**
* {@inheritDoc}
@@ -877,6 +926,7 @@ class Field extends AccessibleObject implements Member {
@FastNative
public native Annotation[] getDeclaredAnnotations();
+ // BEGIN Android-added: Methods for use by Android-specific code.
/**
* Returns the index of this field's ID in its dex file.
*
@@ -900,4 +950,5 @@ class Field extends AccessibleObject implements Member {
*/
@FastNative
public native long getArtField();
+ // END Android-added: Methods for use by Android-specific code.
}
diff --git a/java/lang/reflect/MalformedParametersException.java b/java/lang/reflect/MalformedParametersException.java
index 6b14b150..74796244 100644
--- a/java/lang/reflect/MalformedParametersException.java
+++ b/java/lang/reflect/MalformedParametersException.java
@@ -47,7 +47,6 @@ package java.lang.reflect;
*
* @see java.lang.reflect.Executable#getParameters
* @since 1.8
- * @hide Hidden pending tests
*/
public class MalformedParametersException extends RuntimeException {
diff --git a/java/lang/reflect/Method.java b/java/lang/reflect/Method.java
index ddda93d0..3da75c56 100644
--- a/java/lang/reflect/Method.java
+++ b/java/lang/reflect/Method.java
@@ -26,6 +26,7 @@
package java.lang.reflect;
+import sun.reflect.CallerSensitive;
import dalvik.annotation.optimization.FastNative;
import java.lang.annotation.Annotation;
import java.util.Comparator;
@@ -53,6 +54,11 @@ import libcore.util.EmptyArray;
* @author Nakul Saraiya
*/
public final class Method extends Executable {
+ // Android-changed: Extensive modifications made throughout the class for ART.
+ // Android-changed: Many fields and methods removed / modified.
+ // Android-removed: Type annotations runtime code. Not supported on Android.
+ // Android-removed: Declared vs actual parameter annotation indexes handling.
+
/**
* Orders methods by their name, parameters and return type.
*
@@ -86,7 +92,7 @@ public final class Method extends Executable {
@Override
boolean hasGenericInformation() {
- // Android-changed: Signature retrieval is handled in Executable.
+ // Android-changed: hasGenericInformation() implemented using Executable.
return super.hasGenericInformationInternal();
}
@@ -95,7 +101,7 @@ public final class Method extends Executable {
*/
@Override
public Class<?> getDeclaringClass() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getDeclaringClass() implemented using Executable.
return super.getDeclaringClassInternal();
}
@@ -105,7 +111,7 @@ public final class Method extends Executable {
*/
@Override
public String getName() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getName() implemented using Executable.
return getMethodNameInternal();
}
@@ -114,7 +120,7 @@ public final class Method extends Executable {
*/
@Override
public int getModifiers() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getModifiers() implemented using Executable.
return super.getModifiersInternal();
}
@@ -126,7 +132,7 @@ public final class Method extends Executable {
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public TypeVariable<Method>[] getTypeParameters() {
- // Android-changed: This is mostly handled by Executable.
+ // Android-changed: getTypeParameters() partly implemented using Executable.
GenericInfo info = getMethodOrConstructorGenericInfoInternal();
return (TypeVariable<Method>[]) info.formalTypeParameters.clone();
}
@@ -138,6 +144,7 @@ public final class Method extends Executable {
* @return the return type for the method this object represents
*/
public Class<?> getReturnType() {
+ // Android-changed: getReturnType() implemented using Executable.
return getMethodReturnTypeInternal();
}
@@ -166,7 +173,7 @@ public final class Method extends Executable {
* @since 1.5
*/
public Type getGenericReturnType() {
- // Android-changed: Modified implementation to use Executable.
+ // Android-changed: getGenericReturnType() partly implemented using Executable.
return Types.getType(getMethodOrConstructorGenericInfoInternal().genericReturnType);
}
@@ -175,7 +182,7 @@ public final class Method extends Executable {
*/
@Override
public Class<?>[] getParameterTypes() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getParameterTypes() partly implemented using Executable.
Class<?>[] paramTypes = super.getParameterTypesInternal();
if (paramTypes == null) {
return EmptyArray.CLASS;
@@ -189,7 +196,7 @@ public final class Method extends Executable {
* @since 1.8
*/
public int getParameterCount() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getParameterTypes() implemented using Executable.
return super.getParameterCountInternal();
}
@@ -209,6 +216,7 @@ public final class Method extends Executable {
* {@inheritDoc}
*/
@Override
+ // Android-changed: getExceptionTypes() implemented natively.
@FastNative
public native Class<?>[] getExceptionTypes();
@@ -235,9 +243,10 @@ public final class Method extends Executable {
Method other = (Method)obj;
if ((getDeclaringClass() == other.getDeclaringClass())
&& (getName() == other.getName())) {
+ // Android-changed: Use getReturnType() instead of deleted returnType field
if (!getReturnType().equals(other.getReturnType()))
return false;
- // Android-changed: Use getParameterTypes.
+ // Android-changed: Use getParameterTypes() instead of deleted parameterTypes field
return equalParamTypes(getParameterTypes(), other.getParameterTypes());
}
}
@@ -280,7 +289,7 @@ public final class Method extends Executable {
* @jls 8.4.3 Method Modifiers
*/
public String toString() {
- // Android-changed: Use getParameterTypes.
+ // Android-changed: Use getParameterTypes() / getExceptionTypes() instead of deleted fields
return sharedToString(Modifier.methodModifiers(),
isDefault(),
getParameterTypes(),
@@ -403,6 +412,8 @@ public final class Method extends Executable {
* @exception ExceptionInInitializerError if the initialization
* provoked by this method fails.
*/
+ @CallerSensitive
+ // Android-changed: invoke(Object, Object...) implemented natively.
@FastNative
public native Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
@@ -416,7 +427,7 @@ public final class Method extends Executable {
* @since 1.5
*/
public boolean isBridge() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: isBridge() implemented using Executable.
return super.isBridgeMethodInternal();
}
@@ -452,7 +463,7 @@ public final class Method extends Executable {
* @since 1.8
*/
public boolean isDefault() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: isDefault() implemented using Executable.
return super.isDefaultMethodInternal();
}
@@ -470,6 +481,7 @@ public final class Method extends Executable {
* default class value.
* @since 1.5
*/
+ // Android-changed: isDefault() implemented natively.
@FastNative
public native Object getDefaultValue();
@@ -496,10 +508,11 @@ public final class Method extends Executable {
*/
@Override
public Annotation[][] getParameterAnnotations() {
- // Android-changed: This is handled by Executable.
+ // Android-changed: getParameterAnnotations() implemented using Executable.
return super.getParameterAnnotationsInternal();
}
+ // Android-added: equalNameAndParameters(Method) for Proxy support.
/**
* Returns true if this and {@code method} have the same name and the same
* parameters in the same order. Such methods can share implementation if
diff --git a/java/lang/reflect/Modifier.java b/java/lang/reflect/Modifier.java
index e6945052..28d89bb3 100644
--- a/java/lang/reflect/Modifier.java
+++ b/java/lang/reflect/Modifier.java
@@ -42,6 +42,20 @@ package java.lang.reflect;
*/
public class Modifier {
+ // Android-removed: ReflectionFactory bootstrapping code not used on Android.
+ /*
+ /*
+ * Bootstrapping protocol between java.lang and java.lang.reflect
+ * packages
+ *
+ static {
+ sun.reflect.ReflectionFactory factory =
+ AccessController.doPrivileged(
+ new ReflectionFactory.GetReflectionFactoryAction());
+ factory.setLangReflectAccess(new java.lang.reflect.ReflectAccess());
+ }
+ */
+
/**
* Return {@code true} if the integer argument includes the
* {@code public} modifier, {@code false} otherwise.
@@ -126,6 +140,7 @@ public class Modifier {
return (mod & VOLATILE) != 0;
}
+ // Android-added: isConstructor(int) to support DEX-defined modifier flag.
/**
* Returns true if the given modifiers contain {@link Modifier#CONSTRUCTOR}.
* @hide
@@ -332,6 +347,7 @@ public class Modifier {
// they are not Java programming language keywords
static final int BRIDGE = 0x00000040;
static final int VARARGS = 0x00000080;
+ // Android-changed: SYNTHETIC made public for use in tests.
/**
* @hide
*/
@@ -357,6 +373,7 @@ public class Modifier {
// methods return an unchanging values for a given release, but a
// value that can potentially change over time.
+ // Android-added: CONSTRUCTOR to support DEX-defined modifier flag.
/**
* Dex addition to mark instance constructors and static class
* initializer methods.
@@ -364,6 +381,7 @@ public class Modifier {
*/
public static final int CONSTRUCTOR = 0x10000;
+ // Android-added: DEFAULT to support DEX-defined modifier flag.
/**
* Default methods are marked with a synthetic access flag
* to speed up class loading and invocation target lookup.
diff --git a/java/lang/reflect/Parameter.java b/java/lang/reflect/Parameter.java
index 77c26d70..4dc32b4a 100644
--- a/java/lang/reflect/Parameter.java
+++ b/java/lang/reflect/Parameter.java
@@ -41,6 +41,9 @@ import libcore.reflect.AnnotatedElements;
* @since 1.8
*/
public final class Parameter implements AnnotatedElement {
+ // Android-changed: Extensive modifications made throughout the class for ART.
+ // Android-removed: Type annotations runtime code. Not supported on Android.
+ // Android-removed: Annotation retrieval is implemented natively in ART.
private final String name;
private final int modifiers;
@@ -95,7 +98,7 @@ public final class Parameter implements AnnotatedElement {
return executable.hashCode() ^ index;
}
- // Android-changed: Removed references to the class file format.
+ // Android-changed: Removed references in javadoc to the class file format.
/**
* Returns true if the parameter has a name; returns false otherwise.
* Whether a parameter has a name is determined by compiler options
@@ -271,9 +274,10 @@ public final class Parameter implements AnnotatedElement {
*/
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
Objects.requireNonNull(annotationClass);
- // Android-changed: Uses native code to obtain annotation information.
+ // Android-changed: getAnnotation(Class) Uses native code to obtain annotation information.
return getAnnotationNative(executable, index, annotationClass);
}
+ // Android-added: getAnnotation(Class) Uses native code to obtain annotation information.
@FastNative
private static native <A extends Annotation> A getAnnotationNative(
Executable executable, int parameterIndex, Class<A> annotationType);
@@ -284,7 +288,7 @@ public final class Parameter implements AnnotatedElement {
*/
@Override
public <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
- // Android-changed: Uses AnnotatedElements instead.
+ // Android-changed: getAnnotationsByType(Class), Android uses AnnotatedElements instead.
return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
}
@@ -322,4 +326,5 @@ public final class Parameter implements AnnotatedElement {
public Annotation[] getAnnotations() {
return getDeclaredAnnotations();
}
+
}
diff --git a/java/lang/reflect/Proxy.java b/java/lang/reflect/Proxy.java
index 207189da..531ca18d 100644
--- a/java/lang/reflect/Proxy.java
+++ b/java/lang/reflect/Proxy.java
@@ -240,9 +240,6 @@ public class Proxy implements java.io.Serializable {
private static final long serialVersionUID = -2222568056686623797L;
- /** prefix for all proxy class names */
- private final static String proxyClassNamePrefix = "$Proxy";
-
/** parameter types of a proxy class constructor */
private static final Class<?>[] constructorParams =
{ InvocationHandler.class };
@@ -260,31 +257,6 @@ public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
/**
- * Orders methods by their name, parameters, return type and inheritance relationship.
- *
- * @hide
- */
- private static final Comparator<Method> ORDER_BY_SIGNATURE_AND_SUBTYPE = new Comparator<Method>() {
- @Override public int compare(Method a, Method b) {
- int comparison = Method.ORDER_BY_SIGNATURE.compare(a, b);
- if (comparison != 0) {
- return comparison;
- }
- Class<?> aClass = a.getDeclaringClass();
- Class<?> bClass = b.getDeclaringClass();
- if (aClass == bClass) {
- return 0;
- } else if (aClass.isAssignableFrom(bClass)) {
- return 1;
- } else if (bClass.isAssignableFrom(aClass)) {
- return -1;
- } else {
- return 0;
- }
- }
- };
-
- /**
* Prohibits instantiation.
*/
private Proxy() {
@@ -400,8 +372,55 @@ public class Proxy implements java.io.Serializable {
Class<?>... interfaces)
throws IllegalArgumentException
{
+ // BEGIN Android-changed: Excluded SecurityManager / permission checks.
+ /*
+ final Class<?>[] intfs = interfaces.clone();
+ final SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
+ }
+
+ return getProxyClass0(loader, intfs);
+ */
+
return getProxyClass0(loader, interfaces);
+ // END Android-changed: Excluded SecurityManager / permission checks.
+ }
+
+ // Android-removed: SecurityManager / permission check code.
+ /*
+ /*
+ * Check permissions required to create a Proxy class.
+ *
+ * To define a proxy class, it performs the access checks as in
+ * Class.forName (VM will invoke ClassLoader.checkPackageAccess):
+ * 1. "getClassLoader" permission check if loader == null
+ * 2. checkPackageAccess on the interfaces it implements
+ *
+ * To get a constructor and new instance of a proxy class, it performs
+ * the package access check on the interfaces it implements
+ * as in Class.getConstructor.
+ *
+ * If an interface is non-public, the proxy class must be defined by
+ * the defining loader of the interface. If the caller's class loader
+ * is not the same as the defining loader of the interface, the VM
+ * will throw IllegalAccessError when the generated proxy class is
+ * being defined via the defineClass0 method.
+ *
+ private static void checkProxyAccess(Class<?> caller,
+ ClassLoader loader,
+ Class<?>... interfaces)
+ {
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ ClassLoader ccl = caller.getClassLoader();
+ if (VM.isSystemDomainLoader(loader) && !VM.isSystemDomainLoader(ccl)) {
+ sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
+ }
+ ReflectUtil.checkProxyPackageAccess(ccl, interfaces);
+ }
}
+ */
/**
* Generate a proxy class. Must call the checkProxyAccess method
@@ -550,6 +569,32 @@ public class Proxy implements java.io.Serializable {
}
}
+ // BEGIN Android-changed: How proxies are generated.
+ /**
+ * Orders methods by their name, parameters, return type and inheritance relationship.
+ *
+ * @hide
+ */
+ private static final Comparator<Method> ORDER_BY_SIGNATURE_AND_SUBTYPE = new Comparator<Method>() {
+ @Override public int compare(Method a, Method b) {
+ int comparison = Method.ORDER_BY_SIGNATURE.compare(a, b);
+ if (comparison != 0) {
+ return comparison;
+ }
+ Class<?> aClass = a.getDeclaringClass();
+ Class<?> bClass = b.getDeclaringClass();
+ if (aClass == bClass) {
+ return 0;
+ } else if (aClass.isAssignableFrom(bClass)) {
+ return 1;
+ } else if (bClass.isAssignableFrom(aClass)) {
+ return -1;
+ } else {
+ return 0;
+ }
+ }
+ };
+
/**
* A factory function that generates, defines and returns the proxy class given
* the ClassLoader and array of interfaces.
@@ -753,6 +798,12 @@ public class Proxy implements java.io.Serializable {
}
}
+ @FastNative
+ private static native Class<?> generateProxy(String name, Class<?>[] interfaces,
+ ClassLoader loader, Method[] methods,
+ Class<?>[][] exceptions);
+ // END Android-changed: How proxies are generated.
+
/**
* Returns an instance of a proxy class for the specified interfaces
@@ -808,11 +859,13 @@ public class Proxy implements java.io.Serializable {
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
- // Android-changed: sm is always null
- // final SecurityManager sm = System.getSecurityManager();
- // if (sm != null) {
- // checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
- // }
+ // Android-removed: SecurityManager calls
+ /*
+ final SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
+ }
+ */
/*
* Look up or generate the designated proxy class.
@@ -823,16 +876,28 @@ public class Proxy implements java.io.Serializable {
* Invoke its constructor with the designated invocation handler.
*/
try {
- // Android-changed: sm is always null
- // if (sm != null) {
- // checkNewProxyPermission(Reflection.getCallerClass(), cl);
- // }
+ // Android-removed: SecurityManager / permission checks.
+ /*
+ if (sm != null) {
+ checkNewProxyPermission(Reflection.getCallerClass(), cl);
+ }
+ */
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
- // Android-changed: Removed AccessController.doPrivileged
+ // BEGIN Android-changed: Excluded AccessController.doPrivileged call.
+ /*
+ AccessController.doPrivileged(new PrivilegedAction<Void>() {
+ public Void run() {
+ cons.setAccessible(true);
+ return null;
+ }
+ });
+ */
+
cons.setAccessible(true);
+ // END Android-removed: Excluded AccessController.doPrivileged call.
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
@@ -849,6 +914,31 @@ public class Proxy implements java.io.Serializable {
}
}
+ // Android-removed: SecurityManager / permission checks.
+ /*
+ private static void checkNewProxyPermission(Class<?> caller, Class<?> proxyClass) {
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ if (ReflectUtil.isNonPublicProxyClass(proxyClass)) {
+ ClassLoader ccl = caller.getClassLoader();
+ ClassLoader pcl = proxyClass.getClassLoader();
+
+ // do permission check if the caller is in a different runtime package
+ // of the proxy class
+ int n = proxyClass.getName().lastIndexOf('.');
+ String pkg = (n == -1) ? "" : proxyClass.getName().substring(0, n);
+
+ n = caller.getName().lastIndexOf('.');
+ String callerPkg = (n == -1) ? "" : caller.getName().substring(0, n);
+
+ if (pcl != ccl || !pkg.equals(callerPkg)) {
+ sm.checkPermission(new ReflectPermission("newProxyInPackage." + pkg));
+ }
+ }
+ }
+ }
+ */
+
/**
* Returns true if and only if the specified class was dynamically
* generated to be a proxy class using the {@code getProxyClass}
@@ -894,31 +984,25 @@ public class Proxy implements java.io.Serializable {
final Proxy p = (Proxy) proxy;
final InvocationHandler ih = p.h;
- // Android-changed, System.getSecurityManager() is always null
- // if (System.getSecurityManager() != null) {
- // Class<?> ihClass = ih.getClass();
- // Class<?> caller = Reflection.getCallerClass();
- // if (ReflectUtil.needsPackageAccessCheck(caller.getClassLoader(),
- // ihClass.getClassLoader()))
- // {
- // ReflectUtil.checkPackageAccess(ihClass);
- // }
- // }
+ // Android-removed: SecurityManager / access checks.
+ /*
+ if (System.getSecurityManager() != null) {
+ Class<?> ihClass = ih.getClass();
+ Class<?> caller = Reflection.getCallerClass();
+ if (ReflectUtil.needsPackageAccessCheck(caller.getClassLoader(),
+ ihClass.getClassLoader()))
+ {
+ ReflectUtil.checkPackageAccess(ihClass);
+ }
+ }
+ */
+
return ih;
}
- // Android-changed: helper for art native code.
+ // Android-added: Helper method invoke(Proxy, Method, Object[]) for ART native code.
private static Object invoke(Proxy proxy, Method method, Object[] args) throws Throwable {
InvocationHandler h = proxy.h;
return h.invoke(proxy, method, args);
}
-
- @FastNative
- private static native Class<?> generateProxy(String name, Class<?>[] interfaces,
- ClassLoader loader, Method[] methods,
- Class<?>[][] exceptions);
-
- // Temporary methods.
- private static void reserved1() {};
- private static void reserved2() {};
}
diff --git a/java/lang/reflect/Type.java b/java/lang/reflect/Type.java
index fb98684b..02e97b0a 100644
--- a/java/lang/reflect/Type.java
+++ b/java/lang/reflect/Type.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2003, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -42,7 +42,6 @@ public interface Type {
*
* @return a string describing this type
* @since 1.8
- * @hide Pending tests
*/
default String getTypeName() {
return toString();
diff --git a/java/lang/reflect/TypeVariable.java b/java/lang/reflect/TypeVariable.java
index 20372a43..07b375cd 100644
--- a/java/lang/reflect/TypeVariable.java
+++ b/java/lang/reflect/TypeVariable.java
@@ -48,9 +48,9 @@ package java.lang.reflect;
*
* @since 1.5
*/
-// Android-changed: Removed AnnotatedElement super-class due to excluded support
-// for runtime type annotations
-public interface TypeVariable<D extends GenericDeclaration> extends Type {
+// Android-changed: Removed support for type annotations at runtime.
+// Removed AnnotatedElement super-class.
+public interface TypeVariable<D extends GenericDeclaration> extends Type/*, AnnotatedElement*/ {
/**
* Returns an array of {@code Type} objects representing the
* upper bound(s) of this type variable. Note that if no upper bound is
@@ -100,6 +100,6 @@ public interface TypeVariable<D extends GenericDeclaration> extends Type {
* @return an array of objects representing the upper bounds of the type variable
* @since 1.8
*/
- // Android-changed: Excluded support for runtime type annotations
+ // Android-removed: getAnnotatedBounds(), no support for runtime type annotations on Android.
// AnnotatedType[] getAnnotatedBounds();
}
diff --git a/java/net/InMemoryCookieStore.java b/java/net/InMemoryCookieStore.java
index 3ee98dc5..974eeaa9 100644
--- a/java/net/InMemoryCookieStore.java
+++ b/java/net/InMemoryCookieStore.java
@@ -218,7 +218,7 @@ public class InMemoryCookieStore implements CookieStore {
*/
public boolean removeAll() {
lock.lock();
- // BEGIN Android-change: Return false if it's empty.
+ // BEGIN Android-changed: Let removeAll() return false when there are no cookies.
boolean result = false;
try {
@@ -229,7 +229,7 @@ public class InMemoryCookieStore implements CookieStore {
}
return result;
- // END Android-change: Return false if it's empty.
+ // END Android-changed: Let removeAll() return false when there are no cookies.
}
diff --git a/java/nio/Bits.java b/java/nio/Bits.java
index e0c46ce1..8f8767d6 100644
--- a/java/nio/Bits.java
+++ b/java/nio/Bits.java
@@ -37,8 +37,7 @@ import sun.misc.VM;
class Bits { // package-private
- private Bits() {
- }
+ private Bits() { }
// -- Swapping --
@@ -63,27 +62,27 @@ class Bits { // package-private
// -- get/put char --
static private char makeChar(byte b1, byte b0) {
- return (char) ((b1 << 8) | (b0 & 0xff));
+ return (char)((b1 << 8) | (b0 & 0xff));
}
static char getCharL(ByteBuffer bb, int bi) {
return makeChar(bb._get(bi + 1),
- bb._get(bi));
+ bb._get(bi ));
}
static char getCharL(long a) {
return makeChar(_get(a + 1),
- _get(a));
+ _get(a ));
}
static char getCharB(ByteBuffer bb, int bi) {
- return makeChar(bb._get(bi),
- bb._get(bi + 1));
+ return makeChar(bb._get(bi ),
+ bb._get(bi + 1));
}
static char getCharB(long a) {
- return makeChar(_get(a),
- _get(a + 1));
+ return makeChar(_get(a ),
+ _get(a + 1));
}
static char getChar(ByteBuffer bb, int bi, boolean bigEndian) {
@@ -94,31 +93,26 @@ class Bits { // package-private
return bigEndian ? getCharB(a) : getCharL(a);
}
- private static byte char1(char x) {
- return (byte) (x >> 8);
- }
-
- private static byte char0(char x) {
- return (byte) (x);
- }
+ private static byte char1(char x) { return (byte)(x >> 8); }
+ private static byte char0(char x) { return (byte)(x ); }
static void putCharL(ByteBuffer bb, int bi, char x) {
- bb._put(bi, char0(x));
+ bb._put(bi , char0(x));
bb._put(bi + 1, char1(x));
}
static void putCharL(long a, char x) {
- _put(a, char0(x));
+ _put(a , char0(x));
_put(a + 1, char1(x));
}
static void putCharB(ByteBuffer bb, int bi, char x) {
- bb._put(bi, char1(x));
+ bb._put(bi , char1(x));
bb._put(bi + 1, char0(x));
}
static void putCharB(long a, char x) {
- _put(a, char1(x));
+ _put(a , char1(x));
_put(a + 1, char0(x));
}
@@ -140,27 +134,27 @@ class Bits { // package-private
// -- get/put short --
static private short makeShort(byte b1, byte b0) {
- return (short) ((b1 << 8) | (b0 & 0xff));
+ return (short)((b1 << 8) | (b0 & 0xff));
}
static short getShortL(ByteBuffer bb, int bi) {
return makeShort(bb._get(bi + 1),
- bb._get(bi));
+ bb._get(bi ));
}
static short getShortL(long a) {
return makeShort(_get(a + 1),
- _get(a));
+ _get(a ));
}
static short getShortB(ByteBuffer bb, int bi) {
- return makeShort(bb._get(bi),
- bb._get(bi + 1));
+ return makeShort(bb._get(bi ),
+ bb._get(bi + 1));
}
static short getShortB(long a) {
- return makeShort(_get(a),
- _get(a + 1));
+ return makeShort(_get(a ),
+ _get(a + 1));
}
static short getShort(ByteBuffer bb, int bi, boolean bigEndian) {
@@ -171,31 +165,26 @@ class Bits { // package-private
return bigEndian ? getShortB(a) : getShortL(a);
}
- private static byte short1(short x) {
- return (byte) (x >> 8);
- }
-
- private static byte short0(short x) {
- return (byte) (x);
- }
+ private static byte short1(short x) { return (byte)(x >> 8); }
+ private static byte short0(short x) { return (byte)(x ); }
static void putShortL(ByteBuffer bb, int bi, short x) {
- bb._put(bi, short0(x));
+ bb._put(bi , short0(x));
bb._put(bi + 1, short1(x));
}
static void putShortL(long a, short x) {
- _put(a, short0(x));
+ _put(a , short0(x));
_put(a + 1, short1(x));
}
static void putShortB(ByteBuffer bb, int bi, short x) {
- bb._put(bi, short1(x));
+ bb._put(bi , short1(x));
bb._put(bi + 1, short0(x));
}
static void putShortB(long a, short x) {
- _put(a, short1(x));
+ _put(a , short1(x));
_put(a + 1, short0(x));
}
@@ -217,87 +206,76 @@ class Bits { // package-private
// -- get/put int --
static private int makeInt(byte b3, byte b2, byte b1, byte b0) {
- return (((b3) << 24) |
+ return (((b3 ) << 24) |
((b2 & 0xff) << 16) |
- ((b1 & 0xff) << 8) |
- ((b0 & 0xff)));
+ ((b1 & 0xff) << 8) |
+ ((b0 & 0xff) ));
}
static int getIntL(ByteBuffer bb, int bi) {
return makeInt(bb._get(bi + 3),
- bb._get(bi + 2),
- bb._get(bi + 1),
- bb._get(bi));
+ bb._get(bi + 2),
+ bb._get(bi + 1),
+ bb._get(bi ));
}
static int getIntL(long a) {
return makeInt(_get(a + 3),
- _get(a + 2),
- _get(a + 1),
- _get(a));
+ _get(a + 2),
+ _get(a + 1),
+ _get(a ));
}
static int getIntB(ByteBuffer bb, int bi) {
- return makeInt(bb._get(bi),
- bb._get(bi + 1),
- bb._get(bi + 2),
- bb._get(bi + 3));
+ return makeInt(bb._get(bi ),
+ bb._get(bi + 1),
+ bb._get(bi + 2),
+ bb._get(bi + 3));
}
static int getIntB(long a) {
- return makeInt(_get(a),
- _get(a + 1),
- _get(a + 2),
- _get(a + 3));
+ return makeInt(_get(a ),
+ _get(a + 1),
+ _get(a + 2),
+ _get(a + 3));
}
static int getInt(ByteBuffer bb, int bi, boolean bigEndian) {
- return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi);
+ return bigEndian ? getIntB(bb, bi) : getIntL(bb, bi) ;
}
static int getInt(long a, boolean bigEndian) {
- return bigEndian ? getIntB(a) : getIntL(a);
- }
-
- private static byte int3(int x) {
- return (byte) (x >> 24);
- }
-
- private static byte int2(int x) {
- return (byte) (x >> 16);
- }
-
- private static byte int1(int x) {
- return (byte) (x >> 8);
+ return bigEndian ? getIntB(a) : getIntL(a) ;
}
- private static byte int0(int x) {
- return (byte) (x);
- }
+ private static byte int3(int x) { return (byte)(x >> 24); }
+ private static byte int2(int x) { return (byte)(x >> 16); }
+ private static byte int1(int x) { return (byte)(x >> 8); }
+ private static byte int0(int x) { return (byte)(x ); }
static void putIntL(ByteBuffer bb, int bi, int x) {
bb._put(bi + 3, int3(x));
bb._put(bi + 2, int2(x));
bb._put(bi + 1, int1(x));
- bb._put(bi, int0(x));
+ bb._put(bi , int0(x));
}
static void putIntL(long a, int x) {
_put(a + 3, int3(x));
_put(a + 2, int2(x));
_put(a + 1, int1(x));
- _put(a, int0(x));
+ _put(a , int0(x));
}
static void putIntB(ByteBuffer bb, int bi, int x) {
- bb._put(bi, int3(x));
+ bb._put(bi , int3(x));
bb._put(bi + 1, int2(x));
bb._put(bi + 2, int1(x));
bb._put(bi + 3, int0(x));
}
static void putIntB(long a, int x) {
- _put(a, int3(x));
+ _put(a , int3(x));
_put(a + 1, int2(x));
_put(a + 2, int1(x));
_put(a + 3, int0(x));
@@ -321,59 +299,60 @@ class Bits { // package-private
// -- get/put long --
static private long makeLong(byte b7, byte b6, byte b5, byte b4,
- byte b3, byte b2, byte b1, byte b0) {
- return ((((long) b7) << 56) |
- (((long) b6 & 0xff) << 48) |
- (((long) b5 & 0xff) << 40) |
- (((long) b4 & 0xff) << 32) |
- (((long) b3 & 0xff) << 24) |
- (((long) b2 & 0xff) << 16) |
- (((long) b1 & 0xff) << 8) |
- (((long) b0 & 0xff)));
+ byte b3, byte b2, byte b1, byte b0)
+ {
+ return ((((long)b7 ) << 56) |
+ (((long)b6 & 0xff) << 48) |
+ (((long)b5 & 0xff) << 40) |
+ (((long)b4 & 0xff) << 32) |
+ (((long)b3 & 0xff) << 24) |
+ (((long)b2 & 0xff) << 16) |
+ (((long)b1 & 0xff) << 8) |
+ (((long)b0 & 0xff) ));
}
static long getLongL(ByteBuffer bb, int bi) {
return makeLong(bb._get(bi + 7),
- bb._get(bi + 6),
- bb._get(bi + 5),
- bb._get(bi + 4),
- bb._get(bi + 3),
- bb._get(bi + 2),
- bb._get(bi + 1),
- bb._get(bi));
+ bb._get(bi + 6),
+ bb._get(bi + 5),
+ bb._get(bi + 4),
+ bb._get(bi + 3),
+ bb._get(bi + 2),
+ bb._get(bi + 1),
+ bb._get(bi ));
}
static long getLongL(long a) {
return makeLong(_get(a + 7),
- _get(a + 6),
- _get(a + 5),
- _get(a + 4),
- _get(a + 3),
- _get(a + 2),
- _get(a + 1),
- _get(a));
+ _get(a + 6),
+ _get(a + 5),
+ _get(a + 4),
+ _get(a + 3),
+ _get(a + 2),
+ _get(a + 1),
+ _get(a ));
}
static long getLongB(ByteBuffer bb, int bi) {
- return makeLong(bb._get(bi),
- bb._get(bi + 1),
- bb._get(bi + 2),
- bb._get(bi + 3),
- bb._get(bi + 4),
- bb._get(bi + 5),
- bb._get(bi + 6),
- bb._get(bi + 7));
+ return makeLong(bb._get(bi ),
+ bb._get(bi + 1),
+ bb._get(bi + 2),
+ bb._get(bi + 3),
+ bb._get(bi + 4),
+ bb._get(bi + 5),
+ bb._get(bi + 6),
+ bb._get(bi + 7));
}
static long getLongB(long a) {
- return makeLong(_get(a),
- _get(a + 1),
- _get(a + 2),
- _get(a + 3),
- _get(a + 4),
- _get(a + 5),
- _get(a + 6),
- _get(a + 7));
+ return makeLong(_get(a ),
+ _get(a + 1),
+ _get(a + 2),
+ _get(a + 3),
+ _get(a + 4),
+ _get(a + 5),
+ _get(a + 6),
+ _get(a + 7));
}
static long getLong(ByteBuffer bb, int bi, boolean bigEndian) {
@@ -384,37 +363,14 @@ class Bits { // package-private
return bigEndian ? getLongB(a) : getLongL(a);
}
- private static byte long7(long x) {
- return (byte) (x >> 56);
- }
-
- private static byte long6(long x) {
- return (byte) (x >> 48);
- }
-
- private static byte long5(long x) {
- return (byte) (x >> 40);
- }
-
- private static byte long4(long x) {
- return (byte) (x >> 32);
- }
-
- private static byte long3(long x) {
- return (byte) (x >> 24);
- }
-
- private static byte long2(long x) {
- return (byte) (x >> 16);
- }
-
- private static byte long1(long x) {
- return (byte) (x >> 8);
- }
-
- private static byte long0(long x) {
- return (byte) (x);
- }
+ private static byte long7(long x) { return (byte)(x >> 56); }
+ private static byte long6(long x) { return (byte)(x >> 48); }
+ private static byte long5(long x) { return (byte)(x >> 40); }
+ private static byte long4(long x) { return (byte)(x >> 32); }
+ private static byte long3(long x) { return (byte)(x >> 24); }
+ private static byte long2(long x) { return (byte)(x >> 16); }
+ private static byte long1(long x) { return (byte)(x >> 8); }
+ private static byte long0(long x) { return (byte)(x ); }
static void putLongL(ByteBuffer bb, int bi, long x) {
bb._put(bi + 7, long7(x));
@@ -424,7 +380,7 @@ class Bits { // package-private
bb._put(bi + 3, long3(x));
bb._put(bi + 2, long2(x));
bb._put(bi + 1, long1(x));
- bb._put(bi, long0(x));
+ bb._put(bi , long0(x));
}
static void putLongL(long a, long x) {
@@ -435,11 +391,11 @@ class Bits { // package-private
_put(a + 3, long3(x));
_put(a + 2, long2(x));
_put(a + 1, long1(x));
- _put(a, long0(x));
+ _put(a , long0(x));
}
static void putLongB(ByteBuffer bb, int bi, long x) {
- bb._put(bi, long7(x));
+ bb._put(bi , long7(x));
bb._put(bi + 1, long6(x));
bb._put(bi + 2, long5(x));
bb._put(bi + 3, long4(x));
@@ -450,7 +406,7 @@ class Bits { // package-private
}
static void putLongB(long a, long x) {
- _put(a, long7(x));
+ _put(a , long7(x));
_put(a + 1, long6(x));
_put(a + 2, long5(x));
_put(a + 3, long4(x));
@@ -667,7 +623,7 @@ class Bits { // package-private
}
static int pageCount(long size) {
- return (int) (size + (long) pageSize() - 1L) / pageSize();
+ return (int)(size + (long)pageSize() - 1L) / pageSize();
}
private static boolean unaligned;
@@ -677,9 +633,9 @@ class Bits { // package-private
if (unalignedKnown)
return unaligned;
String arch = AccessController.doPrivileged(
- new sun.security.action.GetPropertyAction("os.arch"));
+ new sun.security.action.GetPropertyAction("os.arch"));
unaligned = arch.equals("i386") || arch.equals("x86")
- || arch.equals("amd64") || arch.equals("x86_64");
+ || arch.equals("amd64") || arch.equals("x86_64");
unalignedKnown = true;
return unaligned;
}
@@ -716,6 +672,7 @@ class Bits { // package-private
}
}
+ // trigger VM's Reference processing
System.gc();
try {
Thread.sleep(100);
@@ -759,15 +716,15 @@ class Bits { // package-private
}
@Override
public long getCount() {
- return Bits.count;
+ return Bits.count.get();
}
@Override
public long getTotalCapacity() {
- return Bits.totalCapacity;
+ return Bits.totalCapacity.get();
}
@Override
public long getMemoryUsed() {
- return Bits.reservedMemory;
+ return Bits.reservedMemory.get();
}
};
}
@@ -789,7 +746,7 @@ class Bits { // package-private
// These numbers represent the point at which we have empirically
// determined that the average cost of a JNI call exceeds the expense
// of an element by element copy. These numbers may change over time.
- static final int JNI_COPY_TO_ARRAY_THRESHOLD = 6;
+ static final int JNI_COPY_TO_ARRAY_THRESHOLD = 6;
static final int JNI_COPY_FROM_ARRAY_THRESHOLD = 6;
// This number limits the number of bytes to copy per call to Unsafe's
@@ -804,14 +761,20 @@ class Bits { // package-private
/**
* Copy from given source array to destination address.
*
- * @param src source array
- * @param srcBaseOffset offset of first element of storage in source array
- * @param srcPos offset within source array of the first element to read
- * @param dstAddr destination address
- * @param length number of bytes to copy
+ * @param src
+ * source array
+ * @param srcBaseOffset
+ * offset of first element of storage in source array
+ * @param srcPos
+ * offset within source array of the first element to read
+ * @param dstAddr
+ * destination address
+ * @param length
+ * number of bytes to copy
*/
static void copyFromArray(Object src, long srcBaseOffset, long srcPos,
- long dstAddr, long length) {
+ long dstAddr, long length)
+ {
long offset = srcBaseOffset + srcPos;
while (length > 0) {
long size = (length > UNSAFE_COPY_THRESHOLD) ? UNSAFE_COPY_THRESHOLD : length;
@@ -825,14 +788,20 @@ class Bits { // package-private
/**
* Copy from source address into given destination array.
*
- * @param srcAddr source address
- * @param dst destination array
- * @param dstBaseOffset offset of first element of storage in destination array
- * @param dstPos offset within destination array of the first element to write
- * @param length number of bytes to copy
+ * @param srcAddr
+ * source address
+ * @param dst
+ * destination array
+ * @param dstBaseOffset
+ * offset of first element of storage in destination array
+ * @param dstPos
+ * offset within destination array of the first element to write
+ * @param length
+ * number of bytes to copy
*/
static void copyToArray(long srcAddr, Object dst, long dstBaseOffset, long dstPos,
- long length) {
+ long length)
+ {
long offset = dstBaseOffset + dstPos;
while (length > 0) {
long size = (length > UNSAFE_COPY_THRESHOLD) ? UNSAFE_COPY_THRESHOLD : length;
@@ -844,30 +813,29 @@ class Bits { // package-private
}
static void copyFromCharArray(Object src, long srcPos, long dstAddr,
- long length) {
+ long length)
+ {
copyFromShortArray(src, srcPos, dstAddr, length);
}
static void copyToCharArray(long srcAddr, Object dst, long dstPos,
- long length) {
+ long length)
+ {
copyToShortArray(srcAddr, dst, dstPos, length);
}
static native void copyFromShortArray(Object src, long srcPos, long dstAddr,
long length);
-
static native void copyToShortArray(long srcAddr, Object dst, long dstPos,
long length);
static native void copyFromIntArray(Object src, long srcPos, long dstAddr,
long length);
-
static native void copyToIntArray(long srcAddr, Object dst, long dstPos,
long length);
static native void copyFromLongArray(Object src, long srcPos, long dstAddr,
long length);
-
static native void copyToLongArray(long srcAddr, Object dst, long dstPos,
long length);
diff --git a/java/nio/Buffer.java b/java/nio/Buffer.java
index 0b383aaa..c912aaa5 100644
--- a/java/nio/Buffer.java
+++ b/java/nio/Buffer.java
@@ -37,16 +37,16 @@ import java.util.Spliterator;
*
* <blockquote>
*
- * <p> A buffer's <i>capacity</i> is the number of elements it contains. The
- * capacity of a buffer is never negative and never changes. </p>
+ * <p> A buffer's <i>capacity</i> is the number of elements it contains. The
+ * capacity of a buffer is never negative and never changes. </p>
*
- * <p> A buffer's <i>limit</i> is the index of the first element that should
- * not be read or written. A buffer's limit is never negative and is never
- * greater than its capacity. </p>
+ * <p> A buffer's <i>limit</i> is the index of the first element that should
+ * not be read or written. A buffer's limit is never negative and is never
+ * greater than its capacity. </p>
*
- * <p> A buffer's <i>position</i> is the index of the next element to be
- * read or written. A buffer's position is never negative and is never
- * greater than its limit. </p>
+ * <p> A buffer's <i>position</i> is the index of the next element to be
+ * read or written. A buffer's position is never negative and is never
+ * greater than its limit. </p>
*
* </blockquote>
*
@@ -60,17 +60,17 @@ import java.util.Spliterator;
*
* <blockquote>
*
- * <p> <i>Relative</i> operations read or write one or more elements starting
- * at the current position and then increment the position by the number of
- * elements transferred. If the requested transfer exceeds the limit then a
- * relative <i>get</i> operation throws a {@link BufferUnderflowException}
- * and a relative <i>put</i> operation throws a {@link
- * BufferOverflowException}; in either case, no data is transferred. </p>
+ * <p> <i>Relative</i> operations read or write one or more elements starting
+ * at the current position and then increment the position by the number of
+ * elements transferred. If the requested transfer exceeds the limit then a
+ * relative <i>get</i> operation throws a {@link BufferUnderflowException}
+ * and a relative <i>put</i> operation throws a {@link
+ * BufferOverflowException}; in either case, no data is transferred. </p>
*
- * <p> <i>Absolute</i> operations take an explicit element index and do not
- * affect the position. Absolute <i>get</i> and <i>put</i> operations throw
- * an {@link IndexOutOfBoundsException} if the index argument exceeds the
- * limit. </p>
+ * <p> <i>Absolute</i> operations take an explicit element index and do not
+ * affect the position. Absolute <i>get</i> and <i>put</i> operations throw
+ * an {@link IndexOutOfBoundsException} if the index argument exceeds the
+ * limit. </p>
*
* </blockquote>
*
@@ -96,11 +96,11 @@ import java.util.Spliterator;
* capacity values:
*
* <blockquote>
- * <tt>0</tt> <tt>&lt;=</tt>
- * <i>mark</i> <tt>&lt;=</tt>
- * <i>position</i> <tt>&lt;=</tt>
- * <i>limit</i> <tt>&lt;=</tt>
- * <i>capacity</i>
+ * <tt>0</tt> <tt>&lt;=</tt>
+ * <i>mark</i> <tt>&lt;=</tt>
+ * <i>position</i> <tt>&lt;=</tt>
+ * <i>limit</i> <tt>&lt;=</tt>
+ * <i>capacity</i>
* </blockquote>
*
* <p> A newly-created buffer always has a position of zero and a mark that is
@@ -118,17 +118,17 @@ import java.util.Spliterator;
*
* <ul>
*
- * <li><p> {@link #clear} makes a buffer ready for a new sequence of
- * channel-read or relative <i>put</i> operations: It sets the limit to the
- * capacity and the position to zero. </p></li>
+ * <li><p> {@link #clear} makes a buffer ready for a new sequence of
+ * channel-read or relative <i>put</i> operations: It sets the limit to the
+ * capacity and the position to zero. </p></li>
*
- * <li><p> {@link #flip} makes a buffer ready for a new sequence of
- * channel-write or relative <i>get</i> operations: It sets the limit to the
- * current position and then sets the position to zero. </p></li>
+ * <li><p> {@link #flip} makes a buffer ready for a new sequence of
+ * channel-write or relative <i>get</i> operations: It sets the limit to the
+ * current position and then sets the position to zero. </p></li>
*
- * <li><p> {@link #rewind} makes a buffer ready for re-reading the data that
- * it already contains: It leaves the limit unchanged and sets the position
- * to zero. </p></li>
+ * <li><p> {@link #rewind} makes a buffer ready for re-reading the data that
+ * it already contains: It leaves the limit unchanged and sets the position
+ * to zero. </p></li>
*
* </ul>
*
@@ -167,6 +167,7 @@ import java.util.Spliterator;
* <blockquote><pre>
* b.flip().position(23).limit(42);</pre></blockquote>
*
+ *
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
@@ -183,6 +184,7 @@ public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
+ // Android-changed: position field non-private for use by Android's nio implementation classes.
int position = 0;
private int limit;
private int capacity;
@@ -191,6 +193,7 @@ public abstract class Buffer {
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
+ // Android-added: _elementSizeShift field for NIOAccess class and framework native code.
/**
* The log base 2 of the element size of this buffer. Each typed subclass
* (ByteBuffer, CharBuffer, etc.) is responsible for initializing this
@@ -202,6 +205,7 @@ public abstract class Buffer {
// Creates a new buffer with the given mark, position, limit, and capacity,
// after checking invariants.
//
+ // Android-added: _elementSizeShift field for NIOAccess class and framework native code.
Buffer(int mark, int pos, int lim, int cap, int elementSizeShift) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
@@ -211,16 +215,17 @@ public abstract class Buffer {
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
- + mark + " > " + pos + ")");
+ + mark + " > " + pos + ")");
this.mark = mark;
}
+ // Android-added: _elementSizeShift field for NIOAccess class and framework native code.
_elementSizeShift = elementSizeShift;
}
/**
* Returns this buffer's capacity.
*
- * @return The capacity of this buffer
+ * @return The capacity of this buffer
*/
public final int capacity() {
return capacity;
@@ -229,7 +234,7 @@ public abstract class Buffer {
/**
* Returns this buffer's position.
*
- * @return The position of this buffer
+ * @return The position of this buffer
*/
public final int position() {
return position;
@@ -239,13 +244,18 @@ public abstract class Buffer {
* Sets this buffer's position. If the mark is defined and larger than the
* new position then it is discarded.
*
- * @param newPosition The new position value; must be non-negative
- * and no larger than the current limit
- * @return This buffer
- * @throws IllegalArgumentException If the preconditions on <tt>newPosition</tt> do not hold
+ * @param newPosition
+ * The new position value; must be non-negative
+ * and no larger than the current limit
+ *
+ * @return This buffer
+ *
+ * @throws IllegalArgumentException
+ * If the preconditions on <tt>newPosition</tt> do not hold
*/
public final Buffer position(int newPosition) {
if ((newPosition > limit) || (newPosition < 0))
+ // Android-changed: Improved error message.
throw new IllegalArgumentException("Bad position " + newPosition + "/" + limit);
position = newPosition;
if (mark > position) mark = -1;
@@ -255,7 +265,7 @@ public abstract class Buffer {
/**
* Returns this buffer's limit.
*
- * @return The limit of this buffer
+ * @return The limit of this buffer
*/
public final int limit() {
return limit;
@@ -266,10 +276,14 @@ public abstract class Buffer {
* then it is set to the new limit. If the mark is defined and larger than
* the new limit then it is discarded.
*
- * @param newLimit The new limit value; must be non-negative
- * and no larger than this buffer's capacity
- * @return This buffer
- * @throws IllegalArgumentException If the preconditions on <tt>newLimit</tt> do not hold
+ * @param newLimit
+ * The new limit value; must be non-negative
+ * and no larger than this buffer's capacity
+ *
+ * @return This buffer
+ *
+ * @throws IllegalArgumentException
+ * If the preconditions on <tt>newLimit</tt> do not hold
*/
public final Buffer limit(int newLimit) {
if ((newLimit > capacity) || (newLimit < 0))
@@ -283,7 +297,7 @@ public abstract class Buffer {
/**
* Sets this buffer's mark at its position.
*
- * @return This buffer
+ * @return This buffer
*/
public final Buffer mark() {
mark = position;
@@ -296,8 +310,10 @@ public abstract class Buffer {
* <p> Invoking this method neither changes nor discards the mark's
* value. </p>
*
- * @return This buffer
- * @throws InvalidMarkException If the mark has not been set
+ * @return This buffer
+ *
+ * @throws InvalidMarkException
+ * If the mark has not been set
*/
public final Buffer reset() {
int m = mark;
@@ -322,7 +338,7 @@ public abstract class Buffer {
* is named as if it did because it will most often be used in situations
* in which that might as well be the case. </p>
*
- * @return This buffer
+ * @return This buffer
*/
public final Buffer clear() {
position = 0;
@@ -350,7 +366,7 @@ public abstract class Buffer {
* java.nio.ByteBuffer#compact compact} method when transferring data from
* one place to another. </p>
*
- * @return This buffer
+ * @return This buffer
*/
public final Buffer flip() {
limit = position;
@@ -372,7 +388,7 @@ public abstract class Buffer {
* buf.rewind(); // Rewind buffer
* buf.get(array); // Copy data into array</pre></blockquote>
*
- * @return This buffer
+ * @return This buffer
*/
public final Buffer rewind() {
position = 0;
@@ -384,7 +400,7 @@ public abstract class Buffer {
* Returns the number of elements between the current position and the
* limit.
*
- * @return The number of elements remaining in this buffer
+ * @return The number of elements remaining in this buffer
*/
public final int remaining() {
return limit - position;
@@ -394,8 +410,8 @@ public abstract class Buffer {
* Tells whether there are any elements between the current position and
* the limit.
*
- * @return <tt>true</tt> if, and only if, there is at least one element
- * remaining in this buffer
+ * @return <tt>true</tt> if, and only if, there is at least one element
+ * remaining in this buffer
*/
public final boolean hasRemaining() {
return position < limit;
@@ -404,7 +420,7 @@ public abstract class Buffer {
/**
* Tells whether or not this buffer is read-only.
*
- * @return <tt>true</tt> if, and only if, this buffer is read-only
+ * @return <tt>true</tt> if, and only if, this buffer is read-only
*/
public abstract boolean isReadOnly();
@@ -416,8 +432,9 @@ public abstract class Buffer {
* and {@link #arrayOffset() arrayOffset} methods may safely be invoked.
* </p>
*
- * @return <tt>true</tt> if, and only if, this buffer
- * is backed by an array and is not read-only
+ * @return <tt>true</tt> if, and only if, this buffer
+ * is backed by an array and is not read-only
+ *
* @since 1.6
*/
public abstract boolean hasArray();
@@ -437,9 +454,14 @@ public abstract class Buffer {
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The array that backs this buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The array that backs this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
+ *
* @since 1.6
*/
public abstract Object array();
@@ -455,10 +477,15 @@ public abstract class Buffer {
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The offset within this buffer's array
- * of the first element of the buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The offset within this buffer's array
+ * of the first element of the buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
+ *
* @since 1.6
*/
public abstract int arrayOffset();
@@ -467,7 +494,8 @@ public abstract class Buffer {
* Tells whether or not this buffer is
* <a href="ByteBuffer.html#direct"><i>direct</i></a>.
*
- * @return <tt>true</tt> if, and only if, this buffer is direct
+ * @return <tt>true</tt> if, and only if, this buffer is direct
+ *
* @since 1.6
*/
public abstract boolean isDirect();
@@ -480,7 +508,7 @@ public abstract class Buffer {
* BufferUnderflowException} if it is not smaller than the limit, and then
* increments the position.
*
- * @return The current position value, before it is incremented
+ * @return The current position value, before it is incremented
*/
final int nextGetIndex() { // package-private
if (position >= limit)
@@ -501,7 +529,7 @@ public abstract class Buffer {
* BufferOverflowException} if it is not smaller than the limit, and then
* increments the position.
*
- * @return The current position value, before it is incremented
+ * @return The current position value, before it is incremented
*/
final int nextPutIndex() { // package-private
if (position >= limit)
@@ -560,6 +588,7 @@ public abstract class Buffer {
"off=" + off + ", len=" + len + " out of bounds (size=" + size + ")");
}
+ // Android-added: getElementSizeShift() method for testing.
/**
* For testing only. This field is accessed directly via JNI from frameworks code.
*
diff --git a/java/nio/BufferOverflowException.java b/java/nio/BufferOverflowException.java
index f359e603..d141a82d 100644
--- a/java/nio/BufferOverflowException.java
+++ b/java/nio/BufferOverflowException.java
@@ -38,14 +38,14 @@ package java.nio;
*/
public class BufferOverflowException
- extends RuntimeException {
+ extends RuntimeException
+{
private static final long serialVersionUID = -5484897634319144535L;
/**
* Constructs an instance of this class.
*/
- public BufferOverflowException() {
- }
+ public BufferOverflowException() { }
}
diff --git a/java/nio/BufferUnderflowException.java b/java/nio/BufferUnderflowException.java
index 8226c95e..d7a062a3 100644
--- a/java/nio/BufferUnderflowException.java
+++ b/java/nio/BufferUnderflowException.java
@@ -38,14 +38,14 @@ package java.nio;
*/
public class BufferUnderflowException
- extends RuntimeException {
+ extends RuntimeException
+{
private static final long serialVersionUID = -1713313658691622206L;
/**
* Constructs an instance of this class.
*/
- public BufferUnderflowException() {
- }
+ public BufferUnderflowException() { }
}
diff --git a/java/nio/ByteBuffer.java b/java/nio/ByteBuffer.java
index 7f51a614..e4a8d0f3 100644
--- a/java/nio/ByteBuffer.java
+++ b/java/nio/ByteBuffer.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 The Android Open Source Project
- * Copyright (c) 2000, 2008, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -38,43 +38,47 @@ import libcore.io.Memory;
*
* <ul>
*
- * <li><p> Absolute and relative {@link #get() </code><i>get</i><code>} and
- * {@link #put(byte) </code><i>put</i><code>} methods that read and write
- * single bytes; </p></li>
+ * <li><p> Absolute and relative {@link #get() <i>get</i>} and
+ * {@link #put(byte) <i>put</i>} methods that read and write
+ * single bytes; </p></li>
*
- * <li><p> Relative {@link #get(byte[]) </code><i>bulk get</i><code>}
- * methods that transfer contiguous sequences of bytes from this buffer
- * into an array; </p></li>
+ * <li><p> Relative {@link #get(byte[]) <i>bulk get</i>}
+ * methods that transfer contiguous sequences of bytes from this buffer
+ * into an array; </p></li>
*
- * <li><p> Relative {@link #put(byte[]) </code><i>bulk put</i><code>}
- * methods that transfer contiguous sequences of bytes from a
- * byte array or some other byte
- * buffer into this buffer; </p></li>
+ * <li><p> Relative {@link #put(byte[]) <i>bulk put</i>}
+ * methods that transfer contiguous sequences of bytes from a
+ * byte array or some other byte
+ * buffer into this buffer; </p></li>
*
- * <li><p> Absolute and relative {@link #getChar() </code><i>get</i><code>}
- * and {@link #putChar(char) </code><i>put</i><code>} methods that read and
- * write values of other primitive types, translating them to and from
- * sequences of bytes in a particular byte order; </p></li>
*
- * <li><p> Methods for creating <i><a href="#views">view buffers</a></i>,
- * which allow a byte buffer to be viewed as a buffer containing values of
- * some other primitive type; and </p></li>
+ * <li><p> Absolute and relative {@link #getChar() <i>get</i>}
+ * and {@link #putChar(char) <i>put</i>} methods that read and
+ * write values of other primitive types, translating them to and from
+ * sequences of bytes in a particular byte order; </p></li>
*
+ * <li><p> Methods for creating <i><a href="#views">view buffers</a></i>,
+ * which allow a byte buffer to be viewed as a buffer containing values of
+ * some other primitive type; and </p></li>
*
*
- * <li><p> Methods for {@link #compact </code>compacting<code>}, {@link
- * #duplicate </code>duplicating<code>}, and {@link #slice
- * </code>slicing<code>} a byte buffer. </p></li>
+ * <li><p> Methods for {@link #compact compacting}, {@link
+ * #duplicate duplicating}, and {@link #slice slicing}
+ * a byte buffer. </p></li>
*
* </ul>
*
* <p> Byte buffers can be created either by {@link #allocate
- * </code><i>allocation</i><code>}, which allocates space for the buffer's
- * content, or by {@link #wrap(byte[]) </code><i>wrapping</i><code>} an
+ * <i>allocation</i>}, which allocates space for the buffer's
+ *
+ *
+ * content, or by {@link #wrap(byte[]) <i>wrapping</i>} an
* existing byte array into a buffer.
*
- * <a name="direct">
- * <h4> Direct <i>vs.</i> non-direct buffers </h4>
+ *
+ *
+ * <a name="direct"></a>
+ * <h2> Direct <i>vs.</i> non-direct buffers </h2>
*
* <p> A byte buffer is either <i>direct</i> or <i>non-direct</i>. Given a
* direct byte buffer, the Java virtual machine will make a best effort to
@@ -95,7 +99,7 @@ import libcore.io.Memory;
* buffers only when they yield a measureable gain in program performance.
*
* <p> A direct byte buffer may also be created by {@link
- * java.nio.channels.FileChannel#map </code>mapping<code>} a region of a file
+ * java.nio.channels.FileChannel#map mapping} a region of a file
* directly into memory. An implementation of the Java platform may optionally
* support the creation of direct byte buffers from native code via JNI. If an
* instance of one of these kinds of buffers refers to an inaccessible region
@@ -107,8 +111,9 @@ import libcore.io.Memory;
* invoking its {@link #isDirect isDirect} method. This method is provided so
* that explicit buffer management can be done in performance-critical code.
*
- * <a name="bin">
- * <h4> Access to binary data </h4>
+ *
+ * <a name="bin"></a>
+ * <h2> Access to binary data </h2>
*
* <p> This class defines methods for reading and writing values of all other
* primitive types, except <tt>boolean</tt>. Primitive values are translated
@@ -127,14 +132,14 @@ import libcore.io.Memory;
* float {@link #getFloat()}
* float {@link #getFloat(int) getFloat(int index)}
* void {@link #putFloat(float) putFloat(float f)}
- * void {@link #putFloat(int, float) putFloat(int index, float f)}</pre></blockquote>
+ * void {@link #putFloat(int,float) putFloat(int index, float f)}</pre></blockquote>
*
* <p> Corresponding methods are defined for the types <tt>char</tt>,
* <tt>short</tt>, <tt>int</tt>, <tt>long</tt>, and <tt>double</tt>. The index
* parameters of the absolute <i>get</i> and <i>put</i> methods are in terms of
* bytes rather than of the type being read or written.
*
- * <a name="views">
+ * <a name="views"></a>
*
* <p> For access to homogeneous binary data, that is, sequences of values of
* the same type, this class defines methods that can create <i>views</i> of a
@@ -153,27 +158,31 @@ import libcore.io.Memory;
*
* <ul>
*
- * <li><p> A view buffer is indexed not in terms of bytes but rather in terms
- * of the type-specific size of its values; </p></li>
+ * <li><p> A view buffer is indexed not in terms of bytes but rather in terms
+ * of the type-specific size of its values; </p></li>
*
- * <li><p> A view buffer provides relative bulk <i>get</i> and <i>put</i>
- * methods that can transfer contiguous sequences of values between a buffer
- * and an array or some other buffer of the same type; and </p></li>
+ * <li><p> A view buffer provides relative bulk <i>get</i> and <i>put</i>
+ * methods that can transfer contiguous sequences of values between a buffer
+ * and an array or some other buffer of the same type; and </p></li>
*
- * <li><p> A view buffer is potentially much more efficient because it will
- * be direct if, and only if, its backing byte buffer is direct. </p></li>
+ * <li><p> A view buffer is potentially much more efficient because it will
+ * be direct if, and only if, its backing byte buffer is direct. </p></li>
*
* </ul>
*
* <p> The byte order of a view buffer is fixed to be that of its byte buffer
* at the time that the view is created. </p>
*
- * <h4> Invocation chaining </h4>
+*
+*
+ *
+ * <h2> Invocation chaining </h2>
*
* <p> Methods in this class that do not otherwise have a value to return are
* specified to return the buffer upon which they are invoked. This allows
* method invocations to be chained.
*
+ *
* The sequence of statements
*
* <blockquote><pre>
@@ -186,14 +195,17 @@ import libcore.io.Memory;
* <blockquote><pre>
* bb.putInt(0xCAFEBABE).putShort(3).putShort(45);</pre></blockquote>
*
+ *
+ *
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
*/
public abstract class ByteBuffer
- extends Buffer
- implements Comparable<ByteBuffer> {
+ extends Buffer
+ implements Comparable<ByteBuffer>
+{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
@@ -207,7 +219,8 @@ public abstract class ByteBuffer
// backing array, and array offset
//
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
- byte[] hb, int offset) {
+ byte[] hb, int offset)
+ {
super(mark, pos, lim, cap, 0);
this.hb = hb;
this.offset = offset;
@@ -226,11 +239,15 @@ public abstract class ByteBuffer
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
* initialized to zero. Whether or not it has a
- * {@link #hasArray </code>backing array<code>} is unspecified.
+ * {@link #hasArray backing array} is unspecified.
*
- * @param capacity The new buffer's capacity, in bytes
- * @return The new byte buffer
- * @throws IllegalArgumentException If the <tt>capacity</tt> is a negative integer
+ * @param capacity
+ * The new buffer's capacity, in bytes
+ *
+ * @return The new byte buffer
+ *
+ * @throws IllegalArgumentException
+ * If the <tt>capacity</tt> is a negative integer
*/
public static ByteBuffer allocateDirect(int capacity) {
if (capacity < 0) {
@@ -247,13 +264,16 @@ public abstract class ByteBuffer
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
- * initialized to zero. It will have a {@link #array
- * </code>backing array<code>}, and its {@link #arrayOffset </code>array
- * offset<code>} will be zero.
+ * initialized to zero. It will have a {@link #array backing array},
+ * and its {@link #arrayOffset array offset} will be zero.
+ *
+ * @param capacity
+ * The new buffer's capacity, in bytes
*
- * @param capacity The new buffer's capacity, in bytes
- * @return The new byte buffer
- * @throws IllegalArgumentException If the <tt>capacity</tt> is a negative integer
+ * @return The new byte buffer
+ *
+ * @throws IllegalArgumentException
+ * If the <tt>capacity</tt> is a negative integer
*/
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
@@ -269,24 +289,32 @@ public abstract class ByteBuffer
* and vice versa. The new buffer's capacity will be
* <tt>array.length</tt>, its position will be <tt>offset</tt>, its limit
* will be <tt>offset + length</tt>, and its mark will be undefined. Its
- * {@link #array </code>backing array<code>} will be the given array, and
- * its {@link #arrayOffset </code>array offset<code>} will be zero. </p>
- *
- * @param array The array that will back the new buffer
- * @param offset The offset of the subarray to be used; must be non-negative and
- * no larger than <tt>array.length</tt>. The new buffer's position
- * will be set to this value.
- * @param length The length of the subarray to be used;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>.
- * The new buffer's limit will be set to <tt>offset + length</tt>.
- * @return The new byte buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * {@link #array backing array} will be the given array, and
+ * its {@link #arrayOffset array offset} will be zero. </p>
+ *
+ * @param array
+ * The array that will back the new buffer
+ *
+ * @param offset
+ * The offset of the subarray to be used; must be non-negative and
+ * no larger than <tt>array.length</tt>. The new buffer's position
+ * will be set to this value.
+ *
+ * @param length
+ * The length of the subarray to be used;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>.
+ * The new buffer's limit will be set to <tt>offset + length</tt>.
+ *
+ * @return The new byte buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public static ByteBuffer wrap(byte[] array,
- int offset, int length) {
+ int offset, int length)
+ {
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
@@ -301,12 +329,14 @@ public abstract class ByteBuffer
* that is, modifications to the buffer will cause the array to be modified
* and vice versa. The new buffer's capacity and limit will be
* <tt>array.length</tt>, its position will be zero, and its mark will be
- * undefined. Its {@link #array </code>backing array<code>} will be the
- * given array, and its {@link #arrayOffset </code>array offset<code>} will
+ * undefined. Its {@link #array backing array} will be the
+ * given array, and its {@link #arrayOffset array offset>} will
* be zero. </p>
*
- * @param array The array that will back this buffer
- * @return The new byte buffer
+ * @param array
+ * The array that will back this buffer
+ *
+ * @return The new byte buffer
*/
public static ByteBuffer wrap(byte[] array) {
return wrap(array, 0, array.length);
@@ -328,7 +358,7 @@ public abstract class ByteBuffer
* buffer is direct, and it will be read-only if, and only if, this buffer
* is read-only. </p>
*
- * @return The new byte buffer
+ * @return The new byte buffer
*/
public abstract ByteBuffer slice();
@@ -345,7 +375,7 @@ public abstract class ByteBuffer
* and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return The new byte buffer
+ * @return The new byte buffer
*/
public abstract ByteBuffer duplicate();
@@ -365,7 +395,7 @@ public abstract class ByteBuffer
* <p> If this buffer is itself read-only then this method behaves in
* exactly the same way as the {@link #duplicate duplicate} method. </p>
*
- * @return The new, read-only byte buffer
+ * @return The new, read-only byte buffer
*/
public abstract ByteBuffer asReadOnlyBuffer();
@@ -374,11 +404,12 @@ public abstract class ByteBuffer
/**
* Relative <i>get</i> method. Reads the byte at this buffer's
- * current position, and then increments the position. </p>
+ * current position, and then increments the position.
+ *
+ * @return The byte at the buffer's current position
*
- * @return The byte at the buffer's current position
- * @throws BufferUnderflowException If the buffer's current position is not smaller than its
- * limit
+ * @throws BufferUnderflowException
+ * If the buffer's current position is not smaller than its limit
*/
public abstract byte get();
@@ -388,22 +419,31 @@ public abstract class ByteBuffer
* <p> Writes the given byte into this buffer at the current
* position, and then increments the position. </p>
*
- * @param b The byte to be written
- * @return This buffer
- * @throws BufferOverflowException If this buffer's current position is not smaller than its
- * limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param b
+ * The byte to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If this buffer's current position is not smaller than its limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer put(byte b);
/**
* Absolute <i>get</i> method. Reads the byte at the given
- * index. </p>
+ * index.
+ *
+ * @param index
+ * The index from which the byte will be read
*
- * @param index The index from which the byte will be read
- * @return The byte at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
+ * @return The byte at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
*/
public abstract byte get(int index);
@@ -413,12 +453,20 @@ public abstract class ByteBuffer
* <p> Writes the given byte into this buffer at the given
* index. </p>
*
- * @param index The index at which the byte will be written
- * @param b The byte value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the byte will be written
+ *
+ * @param b
+ * The byte value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer put(int index, byte b);
@@ -444,26 +492,36 @@ public abstract class ByteBuffer
* <tt>src.get(dst,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst[i] = src.get(); </pre>
+ * dst[i] = src.get();
+ * }</pre>
*
* except that it first checks that there are sufficient bytes in
- * this buffer and it is potentially much more efficient. </p>
- *
- * @param dst The array into which bytes are to be written
- * @param offset The offset within the array of the first byte to be
- * written; must be non-negative and no larger than
- * <tt>dst.length</tt>
- * @param length The maximum number of bytes to be written to the given
- * array; must be non-negative and no larger than
- * <tt>dst.length - offset</tt>
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> bytes
- * remaining in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * this buffer and it is potentially much more efficient.
+ *
+ * @param dst
+ * The array into which bytes are to be written
+ *
+ * @param offset
+ * The offset within the array of the first byte to be
+ * written; must be non-negative and no larger than
+ * <tt>dst.length</tt>
+ *
+ * @param length
+ * The maximum number of bytes to be written to the given
+ * array; must be non-negative and no larger than
+ * <tt>dst.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> bytes
+ * remaining in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public ByteBuffer get(byte[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
@@ -485,9 +543,14 @@ public abstract class ByteBuffer
* <pre>
* src.get(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> bytes
- * remaining in this buffer
+ * @param dst
+ * The destination array
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> bytes
+ * remaining in this buffer
*/
public ByteBuffer get(byte[] dst) {
return get(dst, 0, dst.length);
@@ -519,15 +582,23 @@ public abstract class ByteBuffer
* dst.put(src.get()); </pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The source buffer from which bytes are to be read;
- * must not be this buffer
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * for the remaining bytes in the source buffer
- * @throws IllegalArgumentException If the source buffer is this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The source buffer from which bytes are to be read;
+ * must not be this buffer
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ * for the remaining bytes in the source buffer
+ *
+ * @throws IllegalArgumentException
+ * If the source buffer is this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public ByteBuffer put(ByteBuffer src) {
if (!isAccessible()) {
@@ -593,25 +664,37 @@ public abstract class ByteBuffer
* <tt>dst.put(src,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst.put(a[i]); </pre>
+ * dst.put(a[i]);
+ * }</pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The array from which bytes are to be read
- * @param offset The offset within the array of the first byte to be read;
- * must be non-negative and no larger than <tt>array.length</tt>
- * @param length The number of bytes to be read from the given array;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The array from which bytes are to be read
+ *
+ * @param offset
+ * The offset within the array of the first byte to be read;
+ * must be non-negative and no larger than <tt>array.length</tt>
+ *
+ * @param length
+ * The number of bytes to be read from the given array;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public ByteBuffer put(byte[] src, int offset, int length) {
checkBounds(offset, length, src.length);
@@ -634,9 +717,16 @@ public abstract class ByteBuffer
* <pre>
* dst.put(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param src
+ * The source array
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public final ByteBuffer put(byte[] src) {
return put(src, 0, src.length);
@@ -653,11 +743,11 @@ public abstract class ByteBuffer
* and {@link #arrayOffset() arrayOffset} methods may safely be invoked.
* </p>
*
- * @return <tt>true</tt> if, and only if, this buffer
- * is backed by an array and is not read-only
+ * @return <tt>true</tt> if, and only if, this buffer
+ * is backed by an array and is not read-only
*/
public final boolean hasArray() {
- return (hb != null) && !isReadOnly();
+ return (hb != null) && !isReadOnly;
}
/**
@@ -671,9 +761,13 @@ public abstract class ByteBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The array that backs this buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The array that backs this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final byte[] array() {
if (hb == null)
@@ -694,10 +788,14 @@ public abstract class ByteBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The offset within this buffer's array
- * of the first element of the buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The offset within this buffer's array
+ * of the first element of the buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final int arrayOffset() {
if (hb == null)
@@ -725,37 +823,42 @@ public abstract class ByteBuffer
* followed immediately by an invocation of another relative <i>put</i>
* method. </p>
*
- *
+
*
* <p> Invoke this method after writing data from a buffer in case the
* write was incomplete. The following loop, for example, copies bytes
* from one channel to another via the buffer <tt>buf</tt>:
*
- * <blockquote><pre>
- * buf.clear(); // Prepare buffer for use
- * while (in.read(buf) >= 0 || buf.position != 0) {
- * buf.flip();
- * out.write(buf);
- * buf.compact(); // In case of partial write
+ * <blockquote><pre>{@code
+ * buf.clear(); // Prepare buffer for use
+ * while (in.read(buf) >= 0 || buf.position != 0) {
+ * buf.flip();
+ * out.write(buf);
+ * buf.compact(); // In case of partial write
+ * }
* }</pre></blockquote>
*
- * @return This buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+
+ *
+ * @return This buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer compact();
/**
- * Tells whether or not this byte buffer is direct. </p>
+ * Tells whether or not this byte buffer is direct.
*
- * @return <tt>true</tt> if, and only if, this buffer is direct
+ * @return <tt>true</tt> if, and only if, this buffer is direct
*/
public abstract boolean isDirect();
/**
- * Returns a string summarizing the state of this buffer. </p>
+ * Returns a string summarizing the state of this buffer.
*
- * @return A summary string
+ * @return A summary string
*/
public String toString() {
StringBuffer sb = new StringBuffer();
@@ -782,13 +885,13 @@ public abstract class ByteBuffer
* to use buffers as keys in hash maps or similar data structures unless it
* is known that their contents will not change. </p>
*
- * @return The current hash code of this buffer
+ * @return The current hash code of this buffer
*/
public int hashCode() {
int h = 1;
int p = position();
for (int i = limit() - 1; i >= p; i--)
- h = 31 * h + (int) get(i);
+ h = 31 * h + (int)get(i);
return h;
}
@@ -797,38 +900,33 @@ public abstract class ByteBuffer
*
* <p> Two byte buffers are equal if, and only if,
*
- * <p><ol>
- *
- * <li><p> They have the same element type, </p></li>
- *
- * <li><p> They have the same number of remaining elements, and
- * </p></li>
- *
- * <li><p> The two sequences of remaining elements, considered
- * independently of their starting positions, are pointwise equal.
- *
- *
+ * <ol>
*
+ * <li><p> They have the same element type, </p></li>
*
+ * <li><p> They have the same number of remaining elements, and
+ * </p></li>
*
- *
- *
- * </p></li>
+ * <li><p> The two sequences of remaining elements, considered
+ * independently of their starting positions, are pointwise equal.
+
+ * </p></li>
*
* </ol>
*
* <p> A byte buffer is not equal to any other type of object. </p>
*
- * @param ob The object to which this buffer is to be compared
- * @return <tt>true</tt> if, and only if, this buffer is equal to the
- * given object
+ * @param ob The object to which this buffer is to be compared
+ *
+ * @return <tt>true</tt> if, and only if, this buffer is equal to the
+ * given object
*/
public boolean equals(Object ob) {
if (this == ob)
return true;
if (!(ob instanceof ByteBuffer))
return false;
- ByteBuffer that = (ByteBuffer) ob;
+ ByteBuffer that = (ByteBuffer)ob;
if (this.remaining() != that.remaining())
return false;
int p = this.position();
@@ -860,13 +958,13 @@ public abstract class ByteBuffer
*
*
* Pairs of {@code byte} elements are compared as if by invoking
- * {@link Byte#compare(byte, byte)}.
- *
+ * {@link Byte#compare(byte,byte)}.
+
*
* <p> A byte buffer is not comparable to any other type of object.
*
- * @return A negative integer, zero, or a positive integer as this buffer
- * is less than, equal to, or greater than the given buffer
+ * @return A negative integer, zero, or a positive integer as this buffer
+ * is less than, equal to, or greater than the given buffer
*/
public int compareTo(ByteBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
@@ -892,9 +990,9 @@ public abstract class ByteBuffer
boolean bigEndian // package-private
- = true;
+ = true;
boolean nativeByteOrder // package-private
- = (Bits.byteOrder() == ByteOrder.BIG_ENDIAN);
+ = (Bits.byteOrder() == ByteOrder.BIG_ENDIAN);
/**
* Retrieves this buffer's byte order.
@@ -904,31 +1002,32 @@ public abstract class ByteBuffer
* a newly-created byte buffer is always {@link ByteOrder#BIG_ENDIAN
* BIG_ENDIAN}. </p>
*
- * @return This buffer's byte order
+ * @return This buffer's byte order
*/
public final ByteOrder order() {
return bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
}
/**
- * Modifies this buffer's byte order. </p>
+ * Modifies this buffer's byte order.
+ *
+ * @param bo
+ * The new byte order,
+ * either {@link ByteOrder#BIG_ENDIAN BIG_ENDIAN}
+ * or {@link ByteOrder#LITTLE_ENDIAN LITTLE_ENDIAN}
*
- * @param bo The new byte order,
- * either {@link ByteOrder#BIG_ENDIAN BIG_ENDIAN}
- * or {@link ByteOrder#LITTLE_ENDIAN LITTLE_ENDIAN}
- * @return This buffer
+ * @return This buffer
*/
public final ByteBuffer order(ByteOrder bo) {
bigEndian = (bo == ByteOrder.BIG_ENDIAN);
nativeByteOrder =
- (bigEndian == (Bits.byteOrder() == ByteOrder.BIG_ENDIAN));
+ (bigEndian == (Bits.byteOrder() == ByteOrder.BIG_ENDIAN));
return this;
}
// Unchecked accessors, for use by ByteBufferAs-X-Buffer classes
//
abstract byte _get(int i); // package-private
-
abstract void _put(int i, byte b); // package-private
@@ -939,9 +1038,11 @@ public abstract class ByteBuffer
* composing them into a char value according to the current byte order,
* and then increments the position by two. </p>
*
- * @return The char value at the buffer's current position
- * @throws BufferUnderflowException If there are fewer than two bytes
- * remaining in this buffer
+ * @return The char value at the buffer's current position
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than two bytes
+ * remaining in this buffer
*/
public abstract char getChar();
@@ -953,11 +1054,17 @@ public abstract class ByteBuffer
* current byte order, into this buffer at the current position, and then
* increments the position by two. </p>
*
- * @param value The char value to be written
- * @return This buffer
- * @throws BufferOverflowException If there are fewer than two bytes
- * remaining in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param value
+ * The char value to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there are fewer than two bytes
+ * remaining in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putChar(char value);
@@ -967,11 +1074,15 @@ public abstract class ByteBuffer
* <p> Reads two bytes at the given index, composing them into a
* char value according to the current byte order. </p>
*
- * @param index The index from which the bytes will be read
- * @return The char value at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus one
+ * @param index
+ * The index from which the bytes will be read
+ *
+ * @return The char value at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus one
*/
public abstract char getChar(int index);
@@ -990,13 +1101,21 @@ public abstract class ByteBuffer
* <p> Writes two bytes containing the given char value, in the
* current byte order, into this buffer at the given index. </p>
*
- * @param index The index at which the bytes will be written
- * @param value The char value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus one
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the bytes will be written
+ *
+ * @param value
+ * The char value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus one
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putChar(int index, char value);
@@ -1022,7 +1141,7 @@ public abstract class ByteBuffer
* if, and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return A new char buffer
+ * @return A new char buffer
*/
public abstract CharBuffer asCharBuffer();
@@ -1034,9 +1153,11 @@ public abstract class ByteBuffer
* composing them into a short value according to the current byte order,
* and then increments the position by two. </p>
*
- * @return The short value at the buffer's current position
- * @throws BufferUnderflowException If there are fewer than two bytes
- * remaining in this buffer
+ * @return The short value at the buffer's current position
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than two bytes
+ * remaining in this buffer
*/
public abstract short getShort();
@@ -1048,11 +1169,17 @@ public abstract class ByteBuffer
* current byte order, into this buffer at the current position, and then
* increments the position by two. </p>
*
- * @param value The short value to be written
- * @return This buffer
- * @throws BufferOverflowException If there are fewer than two bytes
- * remaining in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param value
+ * The short value to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there are fewer than two bytes
+ * remaining in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putShort(short value);
@@ -1062,11 +1189,15 @@ public abstract class ByteBuffer
* <p> Reads two bytes at the given index, composing them into a
* short value according to the current byte order. </p>
*
- * @param index The index from which the bytes will be read
- * @return The short value at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus one
+ * @param index
+ * The index from which the bytes will be read
+ *
+ * @return The short value at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus one
*/
public abstract short getShort(int index);
@@ -1085,13 +1216,21 @@ public abstract class ByteBuffer
* <p> Writes two bytes containing the given short value, in the
* current byte order, into this buffer at the given index. </p>
*
- * @param index The index at which the bytes will be written
- * @param value The short value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus one
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the bytes will be written
+ *
+ * @param value
+ * The short value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus one
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putShort(int index, short value);
@@ -1117,7 +1256,7 @@ public abstract class ByteBuffer
* if, and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return A new short buffer
+ * @return A new short buffer
*/
public abstract ShortBuffer asShortBuffer();
@@ -1129,9 +1268,11 @@ public abstract class ByteBuffer
* composing them into an int value according to the current byte order,
* and then increments the position by four. </p>
*
- * @return The int value at the buffer's current position
- * @throws BufferUnderflowException If there are fewer than four bytes
- * remaining in this buffer
+ * @return The int value at the buffer's current position
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than four bytes
+ * remaining in this buffer
*/
public abstract int getInt();
@@ -1143,11 +1284,17 @@ public abstract class ByteBuffer
* current byte order, into this buffer at the current position, and then
* increments the position by four. </p>
*
- * @param value The int value to be written
- * @return This buffer
- * @throws BufferOverflowException If there are fewer than four bytes
- * remaining in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param value
+ * The int value to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there are fewer than four bytes
+ * remaining in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putInt(int value);
@@ -1157,11 +1304,15 @@ public abstract class ByteBuffer
* <p> Reads four bytes at the given index, composing them into a
* int value according to the current byte order. </p>
*
- * @param index The index from which the bytes will be read
- * @return The int value at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus three
+ * @param index
+ * The index from which the bytes will be read
+ *
+ * @return The int value at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus three
*/
public abstract int getInt(int index);
@@ -1180,13 +1331,21 @@ public abstract class ByteBuffer
* <p> Writes four bytes containing the given int value, in the
* current byte order, into this buffer at the given index. </p>
*
- * @param index The index at which the bytes will be written
- * @param value The int value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus three
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the bytes will be written
+ *
+ * @param value
+ * The int value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus three
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putInt(int index, int value);
@@ -1212,7 +1371,7 @@ public abstract class ByteBuffer
* if, and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return A new int buffer
+ * @return A new int buffer
*/
public abstract IntBuffer asIntBuffer();
@@ -1224,9 +1383,11 @@ public abstract class ByteBuffer
* composing them into a long value according to the current byte order,
* and then increments the position by eight. </p>
*
- * @return The long value at the buffer's current position
- * @throws BufferUnderflowException If there are fewer than eight bytes
- * remaining in this buffer
+ * @return The long value at the buffer's current position
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than eight bytes
+ * remaining in this buffer
*/
public abstract long getLong();
@@ -1238,11 +1399,17 @@ public abstract class ByteBuffer
* current byte order, into this buffer at the current position, and then
* increments the position by eight. </p>
*
- * @param value The long value to be written
- * @return This buffer
- * @throws BufferOverflowException If there are fewer than eight bytes
- * remaining in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param value
+ * The long value to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there are fewer than eight bytes
+ * remaining in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putLong(long value);
@@ -1252,11 +1419,15 @@ public abstract class ByteBuffer
* <p> Reads eight bytes at the given index, composing them into a
* long value according to the current byte order. </p>
*
- * @param index The index from which the bytes will be read
- * @return The long value at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus seven
+ * @param index
+ * The index from which the bytes will be read
+ *
+ * @return The long value at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus seven
*/
public abstract long getLong(int index);
@@ -1275,13 +1446,21 @@ public abstract class ByteBuffer
* <p> Writes eight bytes containing the given long value, in the
* current byte order, into this buffer at the given index. </p>
*
- * @param index The index at which the bytes will be written
- * @param value The long value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus seven
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the bytes will be written
+ *
+ * @param value
+ * The long value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus seven
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putLong(int index, long value);
@@ -1307,7 +1486,7 @@ public abstract class ByteBuffer
* if, and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return A new long buffer
+ * @return A new long buffer
*/
public abstract LongBuffer asLongBuffer();
@@ -1319,9 +1498,11 @@ public abstract class ByteBuffer
* composing them into a float value according to the current byte order,
* and then increments the position by four. </p>
*
- * @return The float value at the buffer's current position
- * @throws BufferUnderflowException If there are fewer than four bytes
- * remaining in this buffer
+ * @return The float value at the buffer's current position
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than four bytes
+ * remaining in this buffer
*/
public abstract float getFloat();
@@ -1333,11 +1514,17 @@ public abstract class ByteBuffer
* current byte order, into this buffer at the current position, and then
* increments the position by four. </p>
*
- * @param value The float value to be written
- * @return This buffer
- * @throws BufferOverflowException If there are fewer than four bytes
- * remaining in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param value
+ * The float value to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there are fewer than four bytes
+ * remaining in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putFloat(float value);
@@ -1347,11 +1534,15 @@ public abstract class ByteBuffer
* <p> Reads four bytes at the given index, composing them into a
* float value according to the current byte order. </p>
*
- * @param index The index from which the bytes will be read
- * @return The float value at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus three
+ * @param index
+ * The index from which the bytes will be read
+ *
+ * @return The float value at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus three
*/
public abstract float getFloat(int index);
@@ -1370,13 +1561,21 @@ public abstract class ByteBuffer
* <p> Writes four bytes containing the given float value, in the
* current byte order, into this buffer at the given index. </p>
*
- * @param index The index at which the bytes will be written
- * @param value The float value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus three
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the bytes will be written
+ *
+ * @param value
+ * The float value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus three
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putFloat(int index, float value);
@@ -1402,7 +1601,7 @@ public abstract class ByteBuffer
* if, and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return A new float buffer
+ * @return A new float buffer
*/
public abstract FloatBuffer asFloatBuffer();
@@ -1414,9 +1613,11 @@ public abstract class ByteBuffer
* composing them into a double value according to the current byte order,
* and then increments the position by eight. </p>
*
- * @return The double value at the buffer's current position
- * @throws BufferUnderflowException If there are fewer than eight bytes
- * remaining in this buffer
+ * @return The double value at the buffer's current position
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than eight bytes
+ * remaining in this buffer
*/
public abstract double getDouble();
@@ -1428,11 +1629,17 @@ public abstract class ByteBuffer
* current byte order, into this buffer at the current position, and then
* increments the position by eight. </p>
*
- * @param value The double value to be written
- * @return This buffer
- * @throws BufferOverflowException If there are fewer than eight bytes
- * remaining in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param value
+ * The double value to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there are fewer than eight bytes
+ * remaining in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putDouble(double value);
@@ -1442,11 +1649,15 @@ public abstract class ByteBuffer
* <p> Reads eight bytes at the given index, composing them into a
* double value according to the current byte order. </p>
*
- * @param index The index from which the bytes will be read
- * @return The double value at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus seven
+ * @param index
+ * The index from which the bytes will be read
+ *
+ * @return The double value at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus seven
*/
public abstract double getDouble(int index);
@@ -1465,13 +1676,21 @@ public abstract class ByteBuffer
* <p> Writes eight bytes containing the given double value, in the
* current byte order, into this buffer at the given index. </p>
*
- * @param index The index at which the bytes will be written
- * @param value The double value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit,
- * minus seven
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the bytes will be written
+ *
+ * @param value
+ * The double value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit,
+ * minus seven
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ByteBuffer putDouble(int index, double value);
@@ -1497,7 +1716,7 @@ public abstract class ByteBuffer
* if, and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return A new double buffer
+ * @return A new double buffer
*/
public abstract DoubleBuffer asDoubleBuffer();
diff --git a/java/nio/ByteOrder.java b/java/nio/ByteOrder.java
index 04296dfa..a20cacce 100644
--- a/java/nio/ByteOrder.java
+++ b/java/nio/ByteOrder.java
@@ -47,14 +47,15 @@ public final class ByteOrder {
* multibyte value are ordered from most significant to least significant.
*/
public static final ByteOrder BIG_ENDIAN
- = new ByteOrder("BIG_ENDIAN");
+ = new ByteOrder("BIG_ENDIAN");
/**
* Constant denoting little-endian byte order. In this order, the bytes of
- * a multibyte value are ordered from least significant to most.
+ * a multibyte value are ordered from least significant to most
+ * significant.
*/
public static final ByteOrder LITTLE_ENDIAN
- = new ByteOrder("LITTLE_ENDIAN");
+ = new ByteOrder("LITTLE_ENDIAN");
/**
* Retrieves the native byte order of the underlying platform.
@@ -64,8 +65,8 @@ public final class ByteOrder {
* Native code libraries are often more efficient when such buffers are
* used. </p>
*
- * @return The native byte order of the hardware upon which this Java
- * virtual machine is running
+ * @return The native byte order of the hardware upon which this Java
+ * virtual machine is running
*/
public static ByteOrder nativeOrder() {
return Bits.byteOrder();
@@ -78,7 +79,7 @@ public final class ByteOrder {
* #BIG_ENDIAN} and <tt>"LITTLE_ENDIAN"</tt> for {@link #LITTLE_ENDIAN}.
* </p>
*
- * @return The specified string
+ * @return The specified string
*/
public String toString() {
return name;
diff --git a/java/nio/CharBuffer.java b/java/nio/CharBuffer.java
index 65eb2db6..61265d78 100644
--- a/java/nio/CharBuffer.java
+++ b/java/nio/CharBuffer.java
@@ -43,31 +43,37 @@ import java.util.stream.IntStream;
*
* <ul>
*
- * <li><p> Absolute and relative {@link #get() </code><i>get</i><code>} and
- * {@link #put(char) </code><i>put</i><code>} methods that read and write
- * single chars; </p></li>
+ * <li><p> Absolute and relative {@link #get() <i>get</i>} and
+ * {@link #put(char) <i>put</i>} methods that read and write
+ * single chars; </p></li>
*
- * <li><p> Relative {@link #get(char[]) </code><i>bulk get</i><code>}
- * methods that transfer contiguous sequences of chars from this buffer
- * into an array; and</p></li>
+ * <li><p> Relative {@link #get(char[]) <i>bulk get</i>}
+ * methods that transfer contiguous sequences of chars from this buffer
+ * into an array; and</p></li>
*
- * <li><p> Relative {@link #put(char[]) </code><i>bulk put</i><code>}
- * methods that transfer contiguous sequences of chars from a
- * char array,&#32;a&#32;string, or some other char
- * buffer into this buffer;&#32;and </p></li>
+ * <li><p> Relative {@link #put(char[]) <i>bulk put</i>}
+ * methods that transfer contiguous sequences of chars from a
+ * char array,&#32;a&#32;string, or some other char
+ * buffer into this buffer;&#32;and </p></li>
*
- * <li><p> Methods for {@link #compact </code>compacting<code>}, {@link
- * #duplicate </code>duplicating<code>}, and {@link #slice
- * </code>slicing<code>} a char buffer. </p></li>
+ *
+ * <li><p> Methods for {@link #compact compacting}, {@link
+ * #duplicate duplicating}, and {@link #slice slicing}
+ * a char buffer. </p></li>
*
* </ul>
*
* <p> Char buffers can be created either by {@link #allocate
- * </code><i>allocation</i><code>}, which allocates space for the buffer's
- * content, by {@link #wrap(char[]) </code><i>wrapping</i><code>} an existing
+ * <i>allocation</i>}, which allocates space for the buffer's
+ *
+ *
+ * content, by {@link #wrap(char[]) <i>wrapping</i>} an existing
* char array or&#32;string into a buffer, or by creating a
* <a href="ByteBuffer.html#views"><i>view</i></a> of an existing byte buffer.
*
+ *
+*
+ *
* <p> Like a byte buffer, a char buffer is either <a
* href="ByteBuffer.html#direct"><i>direct</i> or <i>non-direct</i></a>. A
* char buffer created via the <tt>wrap</tt> methods of this class will
@@ -76,11 +82,15 @@ import java.util.stream.IntStream;
* a char buffer is direct may be determined by invoking the {@link
* #isDirect isDirect} method. </p>
*
+*
+ *
* <p> This class implements the {@link CharSequence} interface so that
* character buffers may be used wherever character sequences are accepted, for
* example in the regular-expression package <tt>{@link java.util.regex}</tt>.
* </p>
*
+ *
+ *
* <p> Methods in this class that do not otherwise have a value to return are
* specified to return the buffer upon which they are invoked. This allows
* method invocations to be chained.
@@ -99,14 +109,17 @@ import java.util.stream.IntStream;
* <blockquote><pre>
* cb.put("text/").put(subtype).put("; charset=").put(enc);</pre></blockquote>
*
+ *
+ *
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
*/
public abstract class CharBuffer
- extends Buffer
- implements Comparable<CharBuffer>, Appendable, CharSequence, Readable {
+ extends Buffer
+ implements Comparable<CharBuffer>, Appendable, CharSequence, Readable
+{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
@@ -120,7 +133,8 @@ public abstract class CharBuffer
// backing array, and array offset
//
CharBuffer(int mark, int pos, int lim, int cap, // package-private
- char[] hb, int offset) {
+ char[] hb, int offset)
+ {
super(mark, pos, lim, cap, 1);
this.hb = hb;
this.offset = offset;
@@ -138,13 +152,16 @@ public abstract class CharBuffer
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
- * initialized to zero. It will have a {@link #array
- * </code>backing array<code>}, and its {@link #arrayOffset </code>array
- * offset<code>} will be zero.
+ * initialized to zero. It will have a {@link #array backing array},
+ * and its {@link #arrayOffset array offset} will be zero.
+ *
+ * @param capacity
+ * The new buffer's capacity, in chars
+ *
+ * @return The new char buffer
*
- * @param capacity The new buffer's capacity, in chars
- * @return The new char buffer
- * @throws IllegalArgumentException If the <tt>capacity</tt> is a negative integer
+ * @throws IllegalArgumentException
+ * If the <tt>capacity</tt> is a negative integer
*/
public static CharBuffer allocate(int capacity) {
if (capacity < 0)
@@ -160,24 +177,32 @@ public abstract class CharBuffer
* and vice versa. The new buffer's capacity will be
* <tt>array.length</tt>, its position will be <tt>offset</tt>, its limit
* will be <tt>offset + length</tt>, and its mark will be undefined. Its
- * {@link #array </code>backing array<code>} will be the given array, and
- * its {@link #arrayOffset </code>array offset<code>} will be zero. </p>
- *
- * @param array The array that will back the new buffer
- * @param offset The offset of the subarray to be used; must be non-negative and
- * no larger than <tt>array.length</tt>. The new buffer's position
- * will be set to this value.
- * @param length The length of the subarray to be used;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>.
- * The new buffer's limit will be set to <tt>offset + length</tt>.
- * @return The new char buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * {@link #array backing array} will be the given array, and
+ * its {@link #arrayOffset array offset} will be zero. </p>
+ *
+ * @param array
+ * The array that will back the new buffer
+ *
+ * @param offset
+ * The offset of the subarray to be used; must be non-negative and
+ * no larger than <tt>array.length</tt>. The new buffer's position
+ * will be set to this value.
+ *
+ * @param length
+ * The length of the subarray to be used;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>.
+ * The new buffer's limit will be set to <tt>offset + length</tt>.
+ *
+ * @return The new char buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public static CharBuffer wrap(char[] array,
- int offset, int length) {
+ int offset, int length)
+ {
try {
return new HeapCharBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
@@ -192,12 +217,14 @@ public abstract class CharBuffer
* that is, modifications to the buffer will cause the array to be modified
* and vice versa. The new buffer's capacity and limit will be
* <tt>array.length</tt>, its position will be zero, and its mark will be
- * undefined. Its {@link #array </code>backing array<code>} will be the
- * given array, and its {@link #arrayOffset </code>array offset<code>} will
+ * undefined. Its {@link #array backing array} will be the
+ * given array, and its {@link #arrayOffset array offset>} will
* be zero. </p>
*
- * @param array The array that will back this buffer
- * @return The new char buffer
+ * @param array
+ * The array that will back this buffer
+ *
+ * @return The new char buffer
*/
public static CharBuffer wrap(char[] array) {
return wrap(array, 0, array.length);
@@ -212,9 +239,9 @@ public abstract class CharBuffer
*
* @param target the buffer to read characters into
* @return The number of characters added to the buffer, or
- * -1 if this source of characters is at its end
- * @throws IOException if an I/O error occurs
- * @throws NullPointerException if target is null
+ * -1 if this source of characters is at its end
+ * @throws IOException if an I/O error occurs
+ * @throws NullPointerException if target is null
* @throws ReadOnlyBufferException if target is a read only buffer
* @since 1.5
*/
@@ -246,19 +273,26 @@ public abstract class CharBuffer
* <tt>csq.length()</tt>, its position will be <tt>start</tt>, its limit
* will be <tt>end</tt>, and its mark will be undefined. </p>
*
- * @param csq The character sequence from which the new character buffer is to
- * be created
- * @param start The index of the first character to be used;
- * must be non-negative and no larger than <tt>csq.length()</tt>.
- * The new buffer's position will be set to this value.
- * @param end The index of the character following the last character to be
- * used; must be no smaller than <tt>start</tt> and no larger
- * than <tt>csq.length()</tt>.
- * The new buffer's limit will be set to this value.
- * @return The new character buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>start</tt> and
- * <tt>end</tt>
- * parameters do not hold
+ * @param csq
+ * The character sequence from which the new character buffer is to
+ * be created
+ *
+ * @param start
+ * The index of the first character to be used;
+ * must be non-negative and no larger than <tt>csq.length()</tt>.
+ * The new buffer's position will be set to this value.
+ *
+ * @param end
+ * The index of the character following the last character to be
+ * used; must be no smaller than <tt>start</tt> and no larger
+ * than <tt>csq.length()</tt>.
+ * The new buffer's limit will be set to this value.
+ *
+ * @return The new character buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>start</tt> and <tt>end</tt>
+ * parameters do not hold
*/
public static CharBuffer wrap(CharSequence csq, int start, int end) {
try {
@@ -276,9 +310,11 @@ public abstract class CharBuffer
* <tt>csq.length()</tt>, its position will be zero, and its mark will be
* undefined. </p>
*
- * @param csq The character sequence from which the new character buffer is to
- * be created
- * @return The new character buffer
+ * @param csq
+ * The character sequence from which the new character buffer is to
+ * be created
+ *
+ * @return The new character buffer
*/
public static CharBuffer wrap(CharSequence csq) {
return wrap(csq, 0, csq.length());
@@ -300,7 +336,7 @@ public abstract class CharBuffer
* buffer is direct, and it will be read-only if, and only if, this buffer
* is read-only. </p>
*
- * @return The new char buffer
+ * @return The new char buffer
*/
public abstract CharBuffer slice();
@@ -317,7 +353,7 @@ public abstract class CharBuffer
* and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return The new char buffer
+ * @return The new char buffer
*/
public abstract CharBuffer duplicate();
@@ -337,7 +373,7 @@ public abstract class CharBuffer
* <p> If this buffer is itself read-only then this method behaves in
* exactly the same way as the {@link #duplicate duplicate} method. </p>
*
- * @return The new, read-only char buffer
+ * @return The new, read-only char buffer
*/
public abstract CharBuffer asReadOnlyBuffer();
@@ -346,11 +382,12 @@ public abstract class CharBuffer
/**
* Relative <i>get</i> method. Reads the char at this buffer's
- * current position, and then increments the position. </p>
+ * current position, and then increments the position.
+ *
+ * @return The char at the buffer's current position
*
- * @return The char at the buffer's current position
- * @throws BufferUnderflowException If the buffer's current position is not smaller than its
- * limit
+ * @throws BufferUnderflowException
+ * If the buffer's current position is not smaller than its limit
*/
public abstract char get();
@@ -360,22 +397,31 @@ public abstract class CharBuffer
* <p> Writes the given char into this buffer at the current
* position, and then increments the position. </p>
*
- * @param c The char to be written
- * @return This buffer
- * @throws BufferOverflowException If this buffer's current position is not smaller than its
- * limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param c
+ * The char to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If this buffer's current position is not smaller than its limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract CharBuffer put(char c);
/**
* Absolute <i>get</i> method. Reads the char at the given
- * index. </p>
+ * index.
*
- * @param index The index from which the char will be read
- * @return The char at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
+ * @param index
+ * The index from which the char will be read
+ *
+ * @return The char at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
*/
public abstract char get(int index);
@@ -396,12 +442,20 @@ public abstract class CharBuffer
* <p> Writes the given char into this buffer at the given
* index. </p>
*
- * @param index The index at which the char will be written
- * @param c The char value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the char will be written
+ *
+ * @param c
+ * The char value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract CharBuffer put(int index, char c);
@@ -427,26 +481,36 @@ public abstract class CharBuffer
* <tt>src.get(dst,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst[i] = src.get(); </pre>
+ * dst[i] = src.get();
+ * }</pre>
*
* except that it first checks that there are sufficient chars in
- * this buffer and it is potentially much more efficient. </p>
- *
- * @param dst The array into which chars are to be written
- * @param offset The offset within the array of the first char to be
- * written; must be non-negative and no larger than
- * <tt>dst.length</tt>
- * @param length The maximum number of chars to be written to the given
- * array; must be non-negative and no larger than
- * <tt>dst.length - offset</tt>
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> chars
- * remaining in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * this buffer and it is potentially much more efficient.
+ *
+ * @param dst
+ * The array into which chars are to be written
+ *
+ * @param offset
+ * The offset within the array of the first char to be
+ * written; must be non-negative and no larger than
+ * <tt>dst.length</tt>
+ *
+ * @param length
+ * The maximum number of chars to be written to the given
+ * array; must be non-negative and no larger than
+ * <tt>dst.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> chars
+ * remaining in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public CharBuffer get(char[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
@@ -502,15 +566,23 @@ public abstract class CharBuffer
* dst.put(src.get()); </pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
+ * buffer and it is potentially much more efficient.
*
- * @param src The source buffer from which chars are to be read;
- * must not be this buffer
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * for the remaining chars in the source buffer
- * @throws IllegalArgumentException If the source buffer is this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param src
+ * The source buffer from which chars are to be read;
+ * must not be this buffer
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ * for the remaining chars in the source buffer
+ *
+ * @throws IllegalArgumentException
+ * If the source buffer is this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public CharBuffer put(CharBuffer src) {
if (src == this)
@@ -542,25 +614,37 @@ public abstract class CharBuffer
* <tt>dst.put(src,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst.put(a[i]); </pre>
+ * dst.put(a[i]);
+ * }</pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The array from which chars are to be read
- * @param offset The offset within the array of the first char to be read;
- * must be non-negative and no larger than <tt>array.length</tt>
- * @param length The number of chars to be read from the given array;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The array from which chars are to be read
+ *
+ * @param offset
+ * The offset within the array of the first char to be read;
+ * must be non-negative and no larger than <tt>array.length</tt>
+ *
+ * @param length
+ * The number of chars to be read from the given array;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public CharBuffer put(char[] src, int offset, int length) {
checkBounds(offset, length, src.length);
@@ -583,9 +667,16 @@ public abstract class CharBuffer
* <pre>
* dst.put(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param src
+ * The source array
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public final CharBuffer put(char[] src) {
return put(src, 0, src.length);
@@ -612,26 +703,38 @@ public abstract class CharBuffer
* <tt>dst.put(src,&nbsp;start,&nbsp;end)</tt> has exactly the same effect
* as the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = start; i < end; i++)
- * dst.put(src.charAt(i)); </pre>
+ * dst.put(src.charAt(i));
+ * }</pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The string from which chars are to be read
- * @param start The offset within the string of the first char to be read;
- * must be non-negative and no larger than
- * <tt>string.length()</tt>
- * @param end The offset within the string of the last char to be read,
- * plus one; must be non-negative and no larger than
- * <tt>string.length()</tt>
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>start</tt> and
- * <tt>end</tt>
- * parameters do not hold
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The string from which chars are to be read
+ *
+ * @param start
+ * The offset within the string of the first char to be read;
+ * must be non-negative and no larger than
+ * <tt>string.length()</tt>
+ *
+ * @param end
+ * The offset within the string of the last char to be read,
+ * plus one; must be non-negative and no larger than
+ * <tt>string.length()</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>start</tt> and <tt>end</tt>
+ * parameters do not hold
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public CharBuffer put(String src, int start, int end) {
checkBounds(start, end - start, src.length());
@@ -667,9 +770,16 @@ public abstract class CharBuffer
* <pre>
* dst.put(s, 0, s.length()) </pre>
*
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param src
+ * The source string
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public final CharBuffer put(String src) {
return put(src, 0, src.length());
@@ -686,8 +796,8 @@ public abstract class CharBuffer
* and {@link #arrayOffset() arrayOffset} methods may safely be invoked.
* </p>
*
- * @return <tt>true</tt> if, and only if, this buffer
- * is backed by an array and is not read-only
+ * @return <tt>true</tt> if, and only if, this buffer
+ * is backed by an array and is not read-only
*/
public final boolean hasArray() {
return (hb != null) && !isReadOnly;
@@ -704,9 +814,13 @@ public abstract class CharBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The array that backs this buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The array that backs this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final char[] array() {
if (hb == null)
@@ -727,10 +841,14 @@ public abstract class CharBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The offset within this buffer's array
- * of the first element of the buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The offset within this buffer's array
+ * of the first element of the buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final int arrayOffset() {
if (hb == null)
@@ -758,15 +876,19 @@ public abstract class CharBuffer
* followed immediately by an invocation of another relative <i>put</i>
* method. </p>
*
- * @return This buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+
+ *
+ * @return This buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract CharBuffer compact();
/**
- * Tells whether or not this char buffer is direct. </p>
+ * Tells whether or not this char buffer is direct.
*
- * @return <tt>true</tt> if, and only if, this buffer is direct
+ * @return <tt>true</tt> if, and only if, this buffer is direct
*/
public abstract boolean isDirect();
@@ -782,7 +904,7 @@ public abstract class CharBuffer
* to use buffers as keys in hash maps or similar data structures unless it
* is known that their contents will not change. </p>
*
- * @return The current hash code of this buffer
+ * @return The current hash code of this buffer
*/
public int hashCode() {
int h = 1;
@@ -797,15 +919,15 @@ public abstract class CharBuffer
*
* <p> Two char buffers are equal if, and only if,
*
- * <p><ol>
+ * <ol>
*
- * <li><p> They have the same element type, </p></li>
+ * <li><p> They have the same element type, </p></li>
*
- * <li><p> They have the same number of remaining elements, and
- * </p></li>
+ * <li><p> They have the same number of remaining elements, and
+ * </p></li>
*
- * <li><p> The two sequences of remaining elements, considered
- * independently of their starting positions, are pointwise equal.
+ * <li><p> The two sequences of remaining elements, considered
+ * independently of their starting positions, are pointwise equal.
*
*
*
@@ -813,22 +935,23 @@ public abstract class CharBuffer
*
*
*
- * </p></li>
+ * </p></li>
*
* </ol>
*
* <p> A char buffer is not equal to any other type of object. </p>
*
- * @param ob The object to which this buffer is to be compared
- * @return <tt>true</tt> if, and only if, this buffer is equal to the
- * given object
+ * @param ob The object to which this buffer is to be compared
+ *
+ * @return <tt>true</tt> if, and only if, this buffer is equal to the
+ * given object
*/
public boolean equals(Object ob) {
if (this == ob)
return true;
if (!(ob instanceof CharBuffer))
return false;
- CharBuffer that = (CharBuffer) ob;
+ CharBuffer that = (CharBuffer)ob;
if (this.remaining() != that.remaining())
return false;
int p = this.position();
@@ -860,13 +983,13 @@ public abstract class CharBuffer
*
*
* Pairs of {@code char} elements are compared as if by invoking
- * {@link Character#compare(char, char)}.
- *
+ * {@link Character#compare(char,char)}.
+
*
* <p> A char buffer is not comparable to any other type of object.
*
- * @return A negative integer, zero, or a positive integer as this buffer
- * is less than, equal to, or greater than the given buffer
+ * @return A negative integer, zero, or a positive integer as this buffer
+ * is less than, equal to, or greater than the given buffer
*/
public int compareTo(CharBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
@@ -896,7 +1019,7 @@ public abstract class CharBuffer
* at index <tt>limit()</tt>&nbsp;-&nbsp;1. Invoking this method does not
* change the buffer's position. </p>
*
- * @return The specified string
+ * @return The specified string
*/
public String toString() {
return toString(position(), limit());
@@ -915,7 +1038,7 @@ public abstract class CharBuffer
* (inclusive) and the limit (exclusive); that is, it is equivalent to
* <tt>remaining()</tt>. </p>
*
- * @return The length of this character buffer
+ * @return The length of this character buffer
*/
public final int length() {
return remaining();
@@ -923,13 +1046,17 @@ public abstract class CharBuffer
/**
* Reads the character at the given index relative to the current
- * position. </p>
+ * position.
*
- * @param index The index of the character to be read, relative to the position;
- * must be non-negative and smaller than <tt>remaining()</tt>
- * @return The character at index
- * <tt>position()&nbsp;+&nbsp;index</tt>
- * @throws IndexOutOfBoundsException If the preconditions on <tt>index</tt> do not hold
+ * @param index
+ * The index of the character to be read, relative to the position;
+ * must be non-negative and smaller than <tt>remaining()</tt>
+ *
+ * @return The character at index
+ * <tt>position()&nbsp;+&nbsp;index</tt>
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on <tt>index</tt> do not hold
*/
public final char charAt(int index) {
return get(position() + checkIndex(index, 1));
@@ -948,16 +1075,22 @@ public abstract class CharBuffer
* direct if, and only if, this buffer is direct, and it will be read-only
* if, and only if, this buffer is read-only. </p>
*
- * @param start The index, relative to the current position, of the first
- * character in the subsequence; must be non-negative and no larger
- * than <tt>remaining()</tt>
- * @param end The index, relative to the current position, of the character
- * following the last character in the subsequence; must be no
- * smaller than <tt>start</tt> and no larger than
- * <tt>remaining()</tt>
- * @return The new character buffer
- * @throws IndexOutOfBoundsException If the preconditions on <tt>start</tt> and <tt>end</tt>
- * do not hold
+ * @param start
+ * The index, relative to the current position, of the first
+ * character in the subsequence; must be non-negative and no larger
+ * than <tt>remaining()</tt>
+ *
+ * @param end
+ * The index, relative to the current position, of the character
+ * following the last character in the subsequence; must be no
+ * smaller than <tt>start</tt> and no larger than
+ * <tt>remaining()</tt>
+ *
+ * @return The new character buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on <tt>start</tt> and <tt>end</tt>
+ * do not hold
*/
public abstract CharBuffer subSequence(int start, int end);
@@ -980,13 +1113,20 @@ public abstract class CharBuffer
* toString} method of a character buffer will return a subsequence whose
* content depends upon the buffer's position and limit.
*
- * @param csq The character sequence to append. If <tt>csq</tt> is
- * <tt>null</tt>, then the four characters <tt>"null"</tt> are
- * appended to this character buffer.
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
- * @since 1.5
+ * @param csq
+ * The character sequence to append. If <tt>csq</tt> is
+ * <tt>null</tt>, then the four characters <tt>"null"</tt> are
+ * appended to this character buffer.
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
+ *
+ * @since 1.5
*/
public CharBuffer append(CharSequence csq) {
if (csq == null)
@@ -1006,19 +1146,26 @@ public abstract class CharBuffer
* <pre>
* dst.put(csq.subSequence(start, end).toString()) </pre>
*
- * @param csq The character sequence from which a subsequence will be
- * appended. If <tt>csq</tt> is <tt>null</tt>, then characters
- * will be appended as if <tt>csq</tt> contained the four
- * characters <tt>"null"</tt>.
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws IndexOutOfBoundsException If <tt>start</tt> or <tt>end</tt> are negative,
- * <tt>start</tt>
- * is greater than <tt>end</tt>, or <tt>end</tt> is greater
- * than
- * <tt>csq.length()</tt>
- * @throws ReadOnlyBufferException If this buffer is read-only
- * @since 1.5
+ * @param csq
+ * The character sequence from which a subsequence will be
+ * appended. If <tt>csq</tt> is <tt>null</tt>, then characters
+ * will be appended as if <tt>csq</tt> contained the four
+ * characters <tt>"null"</tt>.
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>start</tt> or <tt>end</tt> are negative, <tt>start</tt>
+ * is greater than <tt>end</tt>, or <tt>end</tt> is greater than
+ * <tt>csq.length()</tt>
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
+ *
+ * @since 1.5
*/
public CharBuffer append(CharSequence csq, int start, int end) {
CharSequence cs = (csq == null ? "null" : csq);
@@ -1035,11 +1182,18 @@ public abstract class CharBuffer
* <pre>
* dst.put(c) </pre>
*
- * @param c The 16-bit char to append
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
- * @since 1.5
+ * @param c
+ * The 16-bit char to append
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
+ *
+ * @since 1.5
*/
public CharBuffer append(char c) {
return put(c);
@@ -1054,12 +1208,12 @@ public abstract class CharBuffer
*
* <p> The byte order of a char buffer created by allocation or by
* wrapping an existing <tt>char</tt> array is the {@link
- * ByteOrder#nativeOrder </code>native order<code>} of the underlying
+ * ByteOrder#nativeOrder native order} of the underlying
* hardware. The byte order of a char buffer created as a <a
* href="ByteBuffer.html#views">view</a> of a byte buffer is that of the
* byte buffer at the moment that the view is created. </p>
*
- * @return This buffer's byte order
+ * @return This buffer's byte order
*/
public abstract ByteOrder order();
diff --git a/java/nio/DoubleBuffer.java b/java/nio/DoubleBuffer.java
index 5f63d24f..d0a0c153 100644
--- a/java/nio/DoubleBuffer.java
+++ b/java/nio/DoubleBuffer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2000, 2008, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -36,30 +36,37 @@ package java.nio;
*
* <ul>
*
- * <li><p> Absolute and relative {@link #get() </code><i>get</i><code>} and
- * {@link #put(double) </code><i>put</i><code>} methods that read and write
- * single doubles; </p></li>
+ * <li><p> Absolute and relative {@link #get() <i>get</i>} and
+ * {@link #put(double) <i>put</i>} methods that read and write
+ * single doubles; </p></li>
*
- * <li><p> Relative {@link #get(double[]) </code><i>bulk get</i><code>}
- * methods that transfer contiguous sequences of doubles from this buffer
- * into an array; and</p></li>
+ * <li><p> Relative {@link #get(double[]) <i>bulk get</i>}
+ * methods that transfer contiguous sequences of doubles from this buffer
+ * into an array; and</p></li>
*
- * <li><p> Relative {@link #put(double[]) </code><i>bulk put</i><code>}
- * methods that transfer contiguous sequences of doubles from a
- * double array or some other double
- * buffer into this buffer;&#32;and </p></li>
+ * <li><p> Relative {@link #put(double[]) <i>bulk put</i>}
+ * methods that transfer contiguous sequences of doubles from a
+ * double array or some other double
+ * buffer into this buffer;&#32;and </p></li>
+ *
+ *
+ * <li><p> Methods for {@link #compact compacting}, {@link
+ * #duplicate duplicating}, and {@link #slice slicing}
+ * a double buffer. </p></li>
*
- * <li><p> Methods for {@link #compact </code>compacting<code>}, {@link
- * #duplicate </code>duplicating<code>}, and {@link #slice
- * </code>slicing<code>} a double buffer. </p></li>
* </ul>
*
* <p> Double buffers can be created either by {@link #allocate
- * </code><i>allocation</i><code>}, which allocates space for the buffer's
- * content, by {@link #wrap(double[]) </code><i>wrapping</i><code>} an existing
+ * <i>allocation</i>}, which allocates space for the buffer's
+ *
+ *
+ * content, by {@link #wrap(double[]) <i>wrapping</i>} an existing
* double array into a buffer, or by creating a
* <a href="ByteBuffer.html#views"><i>view</i></a> of an existing byte buffer.
*
+ *
+*
+ *
* <p> Like a byte buffer, a double buffer is either <a
* href="ByteBuffer.html#direct"><i>direct</i> or <i>non-direct</i></a>. A
* double buffer created via the <tt>wrap</tt> methods of this class will
@@ -68,18 +75,24 @@ package java.nio;
* a double buffer is direct may be determined by invoking the {@link
* #isDirect isDirect} method. </p>
*
+*
+ *
+ *
* <p> Methods in this class that do not otherwise have a value to return are
* specified to return the buffer upon which they are invoked. This allows
* method invocations to be chained.
*
+ *
+ *
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
*/
public abstract class DoubleBuffer
- extends Buffer
- implements Comparable<DoubleBuffer> {
+ extends Buffer
+ implements Comparable<DoubleBuffer>
+{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
@@ -93,7 +106,8 @@ public abstract class DoubleBuffer
// backing array, and array offset
//
DoubleBuffer(int mark, int pos, int lim, int cap, // package-private
- double[] hb, int offset) {
+ double[] hb, int offset)
+ {
super(mark, pos, lim, cap, 3);
this.hb = hb;
this.offset = offset;
@@ -111,13 +125,16 @@ public abstract class DoubleBuffer
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
- * initialized to zero. It will have a {@link #array
- * </code>backing array<code>}, and its {@link #arrayOffset </code>array
- * offset<code>} will be zero.
+ * initialized to zero. It will have a {@link #array backing array},
+ * and its {@link #arrayOffset array offset} will be zero.
*
- * @param capacity The new buffer's capacity, in doubles
- * @return The new double buffer
- * @throws IllegalArgumentException If the <tt>capacity</tt> is a negative integer
+ * @param capacity
+ * The new buffer's capacity, in doubles
+ *
+ * @return The new double buffer
+ *
+ * @throws IllegalArgumentException
+ * If the <tt>capacity</tt> is a negative integer
*/
public static DoubleBuffer allocate(int capacity) {
if (capacity < 0)
@@ -133,24 +150,32 @@ public abstract class DoubleBuffer
* and vice versa. The new buffer's capacity will be
* <tt>array.length</tt>, its position will be <tt>offset</tt>, its limit
* will be <tt>offset + length</tt>, and its mark will be undefined. Its
- * {@link #array </code>backing array<code>} will be the given array, and
- * its {@link #arrayOffset </code>array offset<code>} will be zero. </p>
- *
- * @param array The array that will back the new buffer
- * @param offset The offset of the subarray to be used; must be non-negative and
- * no larger than <tt>array.length</tt>. The new buffer's position
- * will be set to this value.
- * @param length The length of the subarray to be used;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>.
- * The new buffer's limit will be set to <tt>offset + length</tt>.
- * @return The new double buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * {@link #array backing array} will be the given array, and
+ * its {@link #arrayOffset array offset} will be zero. </p>
+ *
+ * @param array
+ * The array that will back the new buffer
+ *
+ * @param offset
+ * The offset of the subarray to be used; must be non-negative and
+ * no larger than <tt>array.length</tt>. The new buffer's position
+ * will be set to this value.
+ *
+ * @param length
+ * The length of the subarray to be used;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>.
+ * The new buffer's limit will be set to <tt>offset + length</tt>.
+ *
+ * @return The new double buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public static DoubleBuffer wrap(double[] array,
- int offset, int length) {
+ int offset, int length)
+ {
try {
return new HeapDoubleBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
@@ -165,12 +190,14 @@ public abstract class DoubleBuffer
* that is, modifications to the buffer will cause the array to be modified
* and vice versa. The new buffer's capacity and limit will be
* <tt>array.length</tt>, its position will be zero, and its mark will be
- * undefined. Its {@link #array </code>backing array<code>} will be the
- * given array, and its {@link #arrayOffset </code>array offset<code>} will
+ * undefined. Its {@link #array backing array} will be the
+ * given array, and its {@link #arrayOffset array offset>} will
* be zero. </p>
*
- * @param array The array that will back this buffer
- * @return The new double buffer
+ * @param array
+ * The array that will back this buffer
+ *
+ * @return The new double buffer
*/
public static DoubleBuffer wrap(double[] array) {
return wrap(array, 0, array.length);
@@ -192,7 +219,7 @@ public abstract class DoubleBuffer
* buffer is direct, and it will be read-only if, and only if, this buffer
* is read-only. </p>
*
- * @return The new double buffer
+ * @return The new double buffer
*/
public abstract DoubleBuffer slice();
@@ -209,7 +236,7 @@ public abstract class DoubleBuffer
* and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return The new double buffer
+ * @return The new double buffer
*/
public abstract DoubleBuffer duplicate();
@@ -229,7 +256,7 @@ public abstract class DoubleBuffer
* <p> If this buffer is itself read-only then this method behaves in
* exactly the same way as the {@link #duplicate duplicate} method. </p>
*
- * @return The new, read-only double buffer
+ * @return The new, read-only double buffer
*/
public abstract DoubleBuffer asReadOnlyBuffer();
@@ -238,11 +265,12 @@ public abstract class DoubleBuffer
/**
* Relative <i>get</i> method. Reads the double at this buffer's
- * current position, and then increments the position. </p>
+ * current position, and then increments the position.
+ *
+ * @return The double at the buffer's current position
*
- * @return The double at the buffer's current position
- * @throws BufferUnderflowException If the buffer's current position is not smaller than its
- * limit
+ * @throws BufferUnderflowException
+ * If the buffer's current position is not smaller than its limit
*/
public abstract double get();
@@ -252,22 +280,31 @@ public abstract class DoubleBuffer
* <p> Writes the given double into this buffer at the current
* position, and then increments the position. </p>
*
- * @param d The double to be written
- * @return This buffer
- * @throws BufferOverflowException If this buffer's current position is not smaller than its
- * limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param d
+ * The double to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If this buffer's current position is not smaller than its limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract DoubleBuffer put(double d);
/**
* Absolute <i>get</i> method. Reads the double at the given
- * index. </p>
+ * index.
+ *
+ * @param index
+ * The index from which the double will be read
*
- * @param index The index from which the double will be read
- * @return The double at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
+ * @return The double at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
*/
public abstract double get(int index);
@@ -277,12 +314,20 @@ public abstract class DoubleBuffer
* <p> Writes the given double into this buffer at the given
* index. </p>
*
- * @param index The index at which the double will be written
- * @param d The double value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the double will be written
+ *
+ * @param d
+ * The double value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract DoubleBuffer put(int index, double d);
@@ -308,26 +353,36 @@ public abstract class DoubleBuffer
* <tt>src.get(dst,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst[i] = src.get(); </pre>
+ * dst[i] = src.get();
+ * }</pre>
*
* except that it first checks that there are sufficient doubles in
- * this buffer and it is potentially much more efficient. </p>
- *
- * @param dst The array into which doubles are to be written
- * @param offset The offset within the array of the first double to be
- * written; must be non-negative and no larger than
- * <tt>dst.length</tt>
- * @param length The maximum number of doubles to be written to the given
- * array; must be non-negative and no larger than
- * <tt>dst.length - offset</tt>
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> doubles
- * remaining in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * this buffer and it is potentially much more efficient.
+ *
+ * @param dst
+ * The array into which doubles are to be written
+ *
+ * @param offset
+ * The offset within the array of the first double to be
+ * written; must be non-negative and no larger than
+ * <tt>dst.length</tt>
+ *
+ * @param length
+ * The maximum number of doubles to be written to the given
+ * array; must be non-negative and no larger than
+ * <tt>dst.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> doubles
+ * remaining in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public DoubleBuffer get(double[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
@@ -349,9 +404,14 @@ public abstract class DoubleBuffer
* <pre>
* src.get(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> doubles
- * remaining in this buffer
+ * @param dst
+ * The destination array
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> doubles
+ * remaining in this buffer
*/
public DoubleBuffer get(double[] dst) {
return get(dst, 0, dst.length);
@@ -383,15 +443,23 @@ public abstract class DoubleBuffer
* dst.put(src.get()); </pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The source buffer from which doubles are to be read;
- * must not be this buffer
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * for the remaining doubles in the source buffer
- * @throws IllegalArgumentException If the source buffer is this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The source buffer from which doubles are to be read;
+ * must not be this buffer
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ * for the remaining doubles in the source buffer
+ *
+ * @throws IllegalArgumentException
+ * If the source buffer is this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public DoubleBuffer put(DoubleBuffer src) {
if (src == this)
@@ -423,25 +491,37 @@ public abstract class DoubleBuffer
* <tt>dst.put(src,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst.put(a[i]); </pre>
+ * dst.put(a[i]);
+ * }</pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The array from which doubles are to be read
- * @param offset The offset within the array of the first double to be read;
- * must be non-negative and no larger than <tt>array.length</tt>
- * @param length The number of doubles to be read from the given array;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The array from which doubles are to be read
+ *
+ * @param offset
+ * The offset within the array of the first double to be read;
+ * must be non-negative and no larger than <tt>array.length</tt>
+ *
+ * @param length
+ * The number of doubles to be read from the given array;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public DoubleBuffer put(double[] src, int offset, int length) {
checkBounds(offset, length, src.length);
@@ -464,9 +544,16 @@ public abstract class DoubleBuffer
* <pre>
* dst.put(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param src
+ * The source array
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public final DoubleBuffer put(double[] src) {
return put(src, 0, src.length);
@@ -483,8 +570,8 @@ public abstract class DoubleBuffer
* and {@link #arrayOffset() arrayOffset} methods may safely be invoked.
* </p>
*
- * @return <tt>true</tt> if, and only if, this buffer
- * is backed by an array and is not read-only
+ * @return <tt>true</tt> if, and only if, this buffer
+ * is backed by an array and is not read-only
*/
public final boolean hasArray() {
return (hb != null) && !isReadOnly;
@@ -501,9 +588,13 @@ public abstract class DoubleBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The array that backs this buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The array that backs this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final double[] array() {
if (hb == null)
@@ -524,10 +615,14 @@ public abstract class DoubleBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The offset within this buffer's array
- * of the first element of the buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The offset within this buffer's array
+ * of the first element of the buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final int arrayOffset() {
if (hb == null)
@@ -555,23 +650,27 @@ public abstract class DoubleBuffer
* followed immediately by an invocation of another relative <i>put</i>
* method. </p>
*
- * @return This buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+
+ *
+ * @return This buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract DoubleBuffer compact();
/**
- * Tells whether or not this double buffer is direct. </p>
+ * Tells whether or not this double buffer is direct.
*
- * @return <tt>true</tt> if, and only if, this buffer is direct
+ * @return <tt>true</tt> if, and only if, this buffer is direct
*/
public abstract boolean isDirect();
/**
- * Returns a string summarizing the state of this buffer. </p>
+ * Returns a string summarizing the state of this buffer.
*
- * @return A summary string
+ * @return A summary string
*/
public String toString() {
StringBuffer sb = new StringBuffer();
@@ -598,7 +697,7 @@ public abstract class DoubleBuffer
* to use buffers as keys in hash maps or similar data structures unless it
* is known that their contents will not change. </p>
*
- * @return The current hash code of this buffer
+ * @return The current hash code of this buffer
*/
public int hashCode() {
int h = 1;
@@ -613,38 +712,39 @@ public abstract class DoubleBuffer
*
* <p> Two double buffers are equal if, and only if,
*
- * <p><ol>
+ * <ol>
*
- * <li><p> They have the same element type, </p></li>
+ * <li><p> They have the same element type, </p></li>
*
- * <li><p> They have the same number of remaining elements, and
- * </p></li>
+ * <li><p> They have the same number of remaining elements, and
+ * </p></li>
*
- * <li><p> The two sequences of remaining elements, considered
- * independently of their starting positions, are pointwise equal.
- *
- * This method considers two double elements {@code a} and {@code b}
- * to be equal if
- * {@code (a == b) || (Double.isNaN(a) && Double.isNaN(b))}.
- * The values {@code -0.0} and {@code +0.0} are considered to be
- * equal, unlike {@link Double#equals(Object)}.
- *
- * </p></li>
+ * <li><p> The two sequences of remaining elements, considered
+ * independently of their starting positions, are pointwise equal.
+
+ * This method considers two double elements {@code a} and {@code b}
+ * to be equal if
+ * {@code (a == b) || (Double.isNaN(a) && Double.isNaN(b))}.
+ * The values {@code -0.0} and {@code +0.0} are considered to be
+ * equal, unlike {@link Double#equals(Object)}.
+
+ * </p></li>
*
* </ol>
*
* <p> A double buffer is not equal to any other type of object. </p>
*
- * @param ob The object to which this buffer is to be compared
- * @return <tt>true</tt> if, and only if, this buffer is equal to the
- * given object
+ * @param ob The object to which this buffer is to be compared
+ *
+ * @return <tt>true</tt> if, and only if, this buffer is equal to the
+ * given object
*/
public boolean equals(Object ob) {
if (this == ob)
return true;
if (!(ob instanceof DoubleBuffer))
return false;
- DoubleBuffer that = (DoubleBuffer) ob;
+ DoubleBuffer that = (DoubleBuffer)ob;
if (this.remaining() != that.remaining())
return false;
int p = this.position();
@@ -667,22 +767,19 @@ public abstract class DoubleBuffer
* <p> Two double buffers are compared by comparing their sequences of
* remaining elements lexicographically, without regard to the starting
* position of each sequence within its corresponding buffer.
- *
+
* Pairs of {@code double} elements are compared as if by invoking
- * {@link Double#compare(double, double)}, except that
+ * {@link Double#compare(double,double)}, except that
* {@code -0.0} and {@code 0.0} are considered to be equal.
* {@code Double.NaN} is considered by this method to be equal
* to itself and greater than all other {@code double} values
* (including {@code Double.POSITIVE_INFINITY}).
- *
- *
- *
- *
+
*
* <p> A double buffer is not comparable to any other type of object.
*
- * @return A negative integer, zero, or a positive integer as this buffer
- * is less than, equal to, or greater than the given buffer
+ * @return A negative integer, zero, or a positive integer as this buffer
+ * is less than, equal to, or greater than the given buffer
*/
public int compareTo(DoubleBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
@@ -698,11 +795,10 @@ public abstract class DoubleBuffer
private static int compare(double x, double y) {
- return ((x < y) ? -1 :
- (x > y) ? +1 :
- (x == y) ? 0 :
- Double.isNaN(x) ? (Double.isNaN(y) ? 0 : +1) : -1);
-
+ return ((x < y) ? -1 :
+ (x > y) ? +1 :
+ (x == y) ? 0 :
+ Double.isNaN(x) ? (Double.isNaN(y) ? 0 : +1) : -1);
}
@@ -717,12 +813,12 @@ public abstract class DoubleBuffer
*
* <p> The byte order of a double buffer created by allocation or by
* wrapping an existing <tt>double</tt> array is the {@link
- * ByteOrder#nativeOrder </code>native order<code>} of the underlying
+ * ByteOrder#nativeOrder native order} of the underlying
* hardware. The byte order of a double buffer created as a <a
* href="ByteBuffer.html#views">view</a> of a byte buffer is that of the
* byte buffer at the moment that the view is created. </p>
*
- * @return This buffer's byte order
+ * @return This buffer's byte order
*/
public abstract ByteOrder order();
diff --git a/java/nio/FloatBuffer.java b/java/nio/FloatBuffer.java
index cd2a502b..90e65ff1 100644
--- a/java/nio/FloatBuffer.java
+++ b/java/nio/FloatBuffer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2000, 2008, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -36,31 +36,37 @@ package java.nio;
*
* <ul>
*
- * <li><p> Absolute and relative {@link #get() </code><i>get</i><code>} and
- * {@link #put(float) </code><i>put</i><code>} methods that read and write
- * single floats; </p></li>
+ * <li><p> Absolute and relative {@link #get() <i>get</i>} and
+ * {@link #put(float) <i>put</i>} methods that read and write
+ * single floats; </p></li>
*
- * <li><p> Relative {@link #get(float[]) </code><i>bulk get</i><code>}
- * methods that transfer contiguous sequences of floats from this buffer
- * into an array; and</p></li>
+ * <li><p> Relative {@link #get(float[]) <i>bulk get</i>}
+ * methods that transfer contiguous sequences of floats from this buffer
+ * into an array; and</p></li>
*
- * <li><p> Relative {@link #put(float[]) </code><i>bulk put</i><code>}
- * methods that transfer contiguous sequences of floats from a
- * float array or some other float
- * buffer into this buffer;&#32;and </p></li>
+ * <li><p> Relative {@link #put(float[]) <i>bulk put</i>}
+ * methods that transfer contiguous sequences of floats from a
+ * float array or some other float
+ * buffer into this buffer;&#32;and </p></li>
*
- * <li><p> Methods for {@link #compact </code>compacting<code>}, {@link
- * #duplicate </code>duplicating<code>}, and {@link #slice
- * </code>slicing<code>} a float buffer. </p></li>
+ *
+ * <li><p> Methods for {@link #compact compacting}, {@link
+ * #duplicate duplicating}, and {@link #slice slicing}
+ * a float buffer. </p></li>
*
* </ul>
*
* <p> Float buffers can be created either by {@link #allocate
- * </code><i>allocation</i><code>}, which allocates space for the buffer's
- * content, by {@link #wrap(float[]) </code><i>wrapping</i><code>} an existing
+ * <i>allocation</i>}, which allocates space for the buffer's
+ *
+ *
+ * content, by {@link #wrap(float[]) <i>wrapping</i>} an existing
* float array into a buffer, or by creating a
* <a href="ByteBuffer.html#views"><i>view</i></a> of an existing byte buffer.
*
+ *
+*
+ *
* <p> Like a byte buffer, a float buffer is either <a
* href="ByteBuffer.html#direct"><i>direct</i> or <i>non-direct</i></a>. A
* float buffer created via the <tt>wrap</tt> methods of this class will
@@ -69,18 +75,24 @@ package java.nio;
* a float buffer is direct may be determined by invoking the {@link
* #isDirect isDirect} method. </p>
*
+*
+ *
+ *
* <p> Methods in this class that do not otherwise have a value to return are
* specified to return the buffer upon which they are invoked. This allows
* method invocations to be chained.
*
+ *
+ *
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
*/
public abstract class FloatBuffer
- extends Buffer
- implements Comparable<FloatBuffer> {
+ extends Buffer
+ implements Comparable<FloatBuffer>
+{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
@@ -94,7 +106,8 @@ public abstract class FloatBuffer
// backing array, and array offset
//
FloatBuffer(int mark, int pos, int lim, int cap, // package-private
- float[] hb, int offset) {
+ float[] hb, int offset)
+ {
super(mark, pos, lim, cap, 2);
this.hb = hb;
this.offset = offset;
@@ -112,13 +125,16 @@ public abstract class FloatBuffer
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
- * initialized to zero. It will have a {@link #array
- * </code>backing array<code>}, and its {@link #arrayOffset </code>array
- * offset<code>} will be zero.
+ * initialized to zero. It will have a {@link #array backing array},
+ * and its {@link #arrayOffset array offset} will be zero.
*
- * @param capacity The new buffer's capacity, in floats
- * @return The new float buffer
- * @throws IllegalArgumentException If the <tt>capacity</tt> is a negative integer
+ * @param capacity
+ * The new buffer's capacity, in floats
+ *
+ * @return The new float buffer
+ *
+ * @throws IllegalArgumentException
+ * If the <tt>capacity</tt> is a negative integer
*/
public static FloatBuffer allocate(int capacity) {
if (capacity < 0)
@@ -134,24 +150,32 @@ public abstract class FloatBuffer
* and vice versa. The new buffer's capacity will be
* <tt>array.length</tt>, its position will be <tt>offset</tt>, its limit
* will be <tt>offset + length</tt>, and its mark will be undefined. Its
- * {@link #array </code>backing array<code>} will be the given array, and
- * its {@link #arrayOffset </code>array offset<code>} will be zero. </p>
- *
- * @param array The array that will back the new buffer
- * @param offset The offset of the subarray to be used; must be non-negative and
- * no larger than <tt>array.length</tt>. The new buffer's position
- * will be set to this value.
- * @param length The length of the subarray to be used;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>.
- * The new buffer's limit will be set to <tt>offset + length</tt>.
- * @return The new float buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * {@link #array backing array} will be the given array, and
+ * its {@link #arrayOffset array offset} will be zero. </p>
+ *
+ * @param array
+ * The array that will back the new buffer
+ *
+ * @param offset
+ * The offset of the subarray to be used; must be non-negative and
+ * no larger than <tt>array.length</tt>. The new buffer's position
+ * will be set to this value.
+ *
+ * @param length
+ * The length of the subarray to be used;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>.
+ * The new buffer's limit will be set to <tt>offset + length</tt>.
+ *
+ * @return The new float buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public static FloatBuffer wrap(float[] array,
- int offset, int length) {
+ int offset, int length)
+ {
try {
return new HeapFloatBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
@@ -166,12 +190,14 @@ public abstract class FloatBuffer
* that is, modifications to the buffer will cause the array to be modified
* and vice versa. The new buffer's capacity and limit will be
* <tt>array.length</tt>, its position will be zero, and its mark will be
- * undefined. Its {@link #array </code>backing array<code>} will be the
- * given array, and its {@link #arrayOffset </code>array offset<code>} will
+ * undefined. Its {@link #array backing array} will be the
+ * given array, and its {@link #arrayOffset array offset>} will
* be zero. </p>
*
- * @param array The array that will back this buffer
- * @return The new float buffer
+ * @param array
+ * The array that will back this buffer
+ *
+ * @return The new float buffer
*/
public static FloatBuffer wrap(float[] array) {
return wrap(array, 0, array.length);
@@ -193,7 +219,7 @@ public abstract class FloatBuffer
* buffer is direct, and it will be read-only if, and only if, this buffer
* is read-only. </p>
*
- * @return The new float buffer
+ * @return The new float buffer
*/
public abstract FloatBuffer slice();
@@ -210,7 +236,7 @@ public abstract class FloatBuffer
* and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return The new float buffer
+ * @return The new float buffer
*/
public abstract FloatBuffer duplicate();
@@ -230,7 +256,7 @@ public abstract class FloatBuffer
* <p> If this buffer is itself read-only then this method behaves in
* exactly the same way as the {@link #duplicate duplicate} method. </p>
*
- * @return The new, read-only float buffer
+ * @return The new, read-only float buffer
*/
public abstract FloatBuffer asReadOnlyBuffer();
@@ -239,11 +265,12 @@ public abstract class FloatBuffer
/**
* Relative <i>get</i> method. Reads the float at this buffer's
- * current position, and then increments the position. </p>
+ * current position, and then increments the position.
+ *
+ * @return The float at the buffer's current position
*
- * @return The float at the buffer's current position
- * @throws BufferUnderflowException If the buffer's current position is not smaller than its
- * limit
+ * @throws BufferUnderflowException
+ * If the buffer's current position is not smaller than its limit
*/
public abstract float get();
@@ -253,22 +280,31 @@ public abstract class FloatBuffer
* <p> Writes the given float into this buffer at the current
* position, and then increments the position. </p>
*
- * @param f The float to be written
- * @return This buffer
- * @throws BufferOverflowException If this buffer's current position is not smaller than its
- * limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param f
+ * The float to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If this buffer's current position is not smaller than its limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract FloatBuffer put(float f);
/**
* Absolute <i>get</i> method. Reads the float at the given
- * index. </p>
+ * index.
+ *
+ * @param index
+ * The index from which the float will be read
*
- * @param index The index from which the float will be read
- * @return The float at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
+ * @return The float at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
*/
public abstract float get(int index);
@@ -278,12 +314,20 @@ public abstract class FloatBuffer
* <p> Writes the given float into this buffer at the given
* index. </p>
*
- * @param index The index at which the float will be written
- * @param f The float value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the float will be written
+ *
+ * @param f
+ * The float value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract FloatBuffer put(int index, float f);
@@ -309,26 +353,36 @@ public abstract class FloatBuffer
* <tt>src.get(dst,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst[i] = src.get(); </pre>
+ * dst[i] = src.get();
+ * }</pre>
*
* except that it first checks that there are sufficient floats in
- * this buffer and it is potentially much more efficient. </p>
- *
- * @param dst The array into which floats are to be written
- * @param offset The offset within the array of the first float to be
- * written; must be non-negative and no larger than
- * <tt>dst.length</tt>
- * @param length The maximum number of floats to be written to the given
- * array; must be non-negative and no larger than
- * <tt>dst.length - offset</tt>
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> floats
- * remaining in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * this buffer and it is potentially much more efficient.
+ *
+ * @param dst
+ * The array into which floats are to be written
+ *
+ * @param offset
+ * The offset within the array of the first float to be
+ * written; must be non-negative and no larger than
+ * <tt>dst.length</tt>
+ *
+ * @param length
+ * The maximum number of floats to be written to the given
+ * array; must be non-negative and no larger than
+ * <tt>dst.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> floats
+ * remaining in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public FloatBuffer get(float[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
@@ -350,9 +404,14 @@ public abstract class FloatBuffer
* <pre>
* src.get(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> floats
- * remaining in this buffer
+ * @param dst
+ * The destination array
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> floats
+ * remaining in this buffer
*/
public FloatBuffer get(float[] dst) {
return get(dst, 0, dst.length);
@@ -384,15 +443,23 @@ public abstract class FloatBuffer
* dst.put(src.get()); </pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The source buffer from which floats are to be read;
- * must not be this buffer
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * for the remaining floats in the source buffer
- * @throws IllegalArgumentException If the source buffer is this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The source buffer from which floats are to be read;
+ * must not be this buffer
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ * for the remaining floats in the source buffer
+ *
+ * @throws IllegalArgumentException
+ * If the source buffer is this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public FloatBuffer put(FloatBuffer src) {
if (src == this)
@@ -424,25 +491,37 @@ public abstract class FloatBuffer
* <tt>dst.put(src,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst.put(a[i]); </pre>
+ * dst.put(a[i]);
+ * }</pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The array from which floats are to be read
- * @param offset The offset within the array of the first float to be read;
- * must be non-negative and no larger than <tt>array.length</tt>
- * @param length The number of floats to be read from the given array;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The array from which floats are to be read
+ *
+ * @param offset
+ * The offset within the array of the first float to be read;
+ * must be non-negative and no larger than <tt>array.length</tt>
+ *
+ * @param length
+ * The number of floats to be read from the given array;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public FloatBuffer put(float[] src, int offset, int length) {
checkBounds(offset, length, src.length);
@@ -465,9 +544,16 @@ public abstract class FloatBuffer
* <pre>
* dst.put(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param src
+ * The source array
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public final FloatBuffer put(float[] src) {
return put(src, 0, src.length);
@@ -484,8 +570,8 @@ public abstract class FloatBuffer
* and {@link #arrayOffset() arrayOffset} methods may safely be invoked.
* </p>
*
- * @return <tt>true</tt> if, and only if, this buffer
- * is backed by an array and is not read-only
+ * @return <tt>true</tt> if, and only if, this buffer
+ * is backed by an array and is not read-only
*/
public final boolean hasArray() {
return (hb != null) && !isReadOnly;
@@ -502,9 +588,13 @@ public abstract class FloatBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The array that backs this buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The array that backs this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final float[] array() {
if (hb == null)
@@ -525,10 +615,14 @@ public abstract class FloatBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The offset within this buffer's array
- * of the first element of the buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The offset within this buffer's array
+ * of the first element of the buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final int arrayOffset() {
if (hb == null)
@@ -556,23 +650,27 @@ public abstract class FloatBuffer
* followed immediately by an invocation of another relative <i>put</i>
* method. </p>
*
- * @return This buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+
+ *
+ * @return This buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract FloatBuffer compact();
/**
- * Tells whether or not this float buffer is direct. </p>
+ * Tells whether or not this float buffer is direct.
*
- * @return <tt>true</tt> if, and only if, this buffer is direct
+ * @return <tt>true</tt> if, and only if, this buffer is direct
*/
public abstract boolean isDirect();
/**
- * Returns a string summarizing the state of this buffer. </p>
+ * Returns a string summarizing the state of this buffer.
*
- * @return A summary string
+ * @return A summary string
*/
public String toString() {
StringBuffer sb = new StringBuffer();
@@ -599,7 +697,7 @@ public abstract class FloatBuffer
* to use buffers as keys in hash maps or similar data structures unless it
* is known that their contents will not change. </p>
*
- * @return The current hash code of this buffer
+ * @return The current hash code of this buffer
*/
public int hashCode() {
int h = 1;
@@ -614,38 +712,39 @@ public abstract class FloatBuffer
*
* <p> Two float buffers are equal if, and only if,
*
- * <p><ol>
- *
- * <li><p> They have the same element type, </p></li>
- *
- * <li><p> They have the same number of remaining elements, and
- * </p></li>
+ * <ol>
*
- * <li><p> The two sequences of remaining elements, considered
- * independently of their starting positions, are pointwise equal.
+ * <li><p> They have the same element type, </p></li>
*
- * This method considers two float elements {@code a} and {@code b}
- * to be equal if
- * {@code (a == b) || (Float.isNaN(a) && Float.isNaN(b))}.
- * The values {@code -0.0} and {@code +0.0} are considered to be
- * equal, unlike {@link Float#equals(Object)}.
+ * <li><p> They have the same number of remaining elements, and
+ * </p></li>
*
- * </p></li>
+ * <li><p> The two sequences of remaining elements, considered
+ * independently of their starting positions, are pointwise equal.
+
+ * This method considers two float elements {@code a} and {@code b}
+ * to be equal if
+ * {@code (a == b) || (Float.isNaN(a) && Float.isNaN(b))}.
+ * The values {@code -0.0} and {@code +0.0} are considered to be
+ * equal, unlike {@link Float#equals(Object)}.
+
+ * </p></li>
*
* </ol>
*
* <p> A float buffer is not equal to any other type of object. </p>
*
- * @param ob The object to which this buffer is to be compared
- * @return <tt>true</tt> if, and only if, this buffer is equal to the
- * given object
+ * @param ob The object to which this buffer is to be compared
+ *
+ * @return <tt>true</tt> if, and only if, this buffer is equal to the
+ * given object
*/
public boolean equals(Object ob) {
if (this == ob)
return true;
if (!(ob instanceof FloatBuffer))
return false;
- FloatBuffer that = (FloatBuffer) ob;
+ FloatBuffer that = (FloatBuffer)ob;
if (this.remaining() != that.remaining())
return false;
int p = this.position();
@@ -668,9 +767,9 @@ public abstract class FloatBuffer
* <p> Two float buffers are compared by comparing their sequences of
* remaining elements lexicographically, without regard to the starting
* position of each sequence within its corresponding buffer.
- *
+
* Pairs of {@code float} elements are compared as if by invoking
- * {@link Float#compare(float, float)}, except that
+ * {@link Float#compare(float,float)}, except that
* {@code -0.0} and {@code 0.0} are considered to be equal.
* {@code Float.NaN} is considered by this method to be equal
* to itself and greater than all other {@code float} values
@@ -682,8 +781,8 @@ public abstract class FloatBuffer
*
* <p> A float buffer is not comparable to any other type of object.
*
- * @return A negative integer, zero, or a positive integer as this buffer
- * is less than, equal to, or greater than the given buffer
+ * @return A negative integer, zero, or a positive integer as this buffer
+ * is less than, equal to, or greater than the given buffer
*/
public int compareTo(FloatBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
@@ -697,25 +796,28 @@ public abstract class FloatBuffer
private static int compare(float x, float y) {
- return ((x < y) ? -1 :
- (x > y) ? +1 :
- (x == y) ? 0 :
- Float.isNaN(x) ? (Float.isNaN(y) ? 0 : +1) : -1);
-
+ return ((x < y) ? -1 :
+ (x > y) ? +1 :
+ (x == y) ? 0 :
+ Float.isNaN(x) ? (Float.isNaN(y) ? 0 : +1) : -1);
}
+ // -- Other char stuff --
+
+ // -- Other byte stuff: Access to binary data --
+
/**
* Retrieves this buffer's byte order.
*
* <p> The byte order of a float buffer created by allocation or by
* wrapping an existing <tt>float</tt> array is the {@link
- * ByteOrder#nativeOrder </code>native order<code>} of the underlying
+ * ByteOrder#nativeOrder native order} of the underlying
* hardware. The byte order of a float buffer created as a <a
* href="ByteBuffer.html#views">view</a> of a byte buffer is that of the
* byte buffer at the moment that the view is created. </p>
*
- * @return This buffer's byte order
+ * @return This buffer's byte order
*/
public abstract ByteOrder order();
diff --git a/java/nio/IntBuffer.java b/java/nio/IntBuffer.java
index 4a209491..a0004bad 100644
--- a/java/nio/IntBuffer.java
+++ b/java/nio/IntBuffer.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 The Android Open Source Project
- * Copyright (c) 2000, 2008, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -37,32 +37,37 @@ package java.nio;
*
* <ul>
*
- * <li><p> Absolute and relative {@link #get() </code><i>get</i><code>} and
- * {@link #put(int) </code><i>put</i><code>} methods that read and write
- * single ints; </p></li>
+ * <li><p> Absolute and relative {@link #get() <i>get</i>} and
+ * {@link #put(int) <i>put</i>} methods that read and write
+ * single ints; </p></li>
*
- * <li><p> Relative {@link #get(int[]) </code><i>bulk get</i><code>}
- * methods that transfer contiguous sequences of ints from this buffer
- * into an array; and</p></li>
+ * <li><p> Relative {@link #get(int[]) <i>bulk get</i>}
+ * methods that transfer contiguous sequences of ints from this buffer
+ * into an array; and</p></li>
*
- * <li><p> Relative {@link #put(int[]) </code><i>bulk put</i><code>}
- * methods that transfer contiguous sequences of ints from an
- * int array or some other int
- * buffer into this buffer;&#32;and </p></li>
+ * <li><p> Relative {@link #put(int[]) <i>bulk put</i>}
+ * methods that transfer contiguous sequences of ints from an
+ * int array or some other int
+ * buffer into this buffer;&#32;and </p></li>
*
- * <li><p> Methods for {@link #compact </code>compacting<code>}, {@link
- * #duplicate </code>duplicating<code>}, and {@link #slice
- * </code>slicing<code>} an int buffer. </p></li>
+ *
+ * <li><p> Methods for {@link #compact compacting}, {@link
+ * #duplicate duplicating}, and {@link #slice slicing}
+ * an int buffer. </p></li>
*
* </ul>
*
* <p> Int buffers can be created either by {@link #allocate
- * </code><i>allocation</i><code>}, which allocates space for the buffer's
+ * <i>allocation</i>}, which allocates space for the buffer's
+ *
*
- * content, by {@link #wrap(int[]) </code><i>wrapping</i><code>} an existing
+ * content, by {@link #wrap(int[]) <i>wrapping</i>} an existing
* int array into a buffer, or by creating a
* <a href="ByteBuffer.html#views"><i>view</i></a> of an existing byte buffer.
*
+ *
+*
+ *
* <p> Like a byte buffer, an int buffer is either <a
* href="ByteBuffer.html#direct"><i>direct</i> or <i>non-direct</i></a>. A
* int buffer created via the <tt>wrap</tt> methods of this class will
@@ -71,18 +76,24 @@ package java.nio;
* an int buffer is direct may be determined by invoking the {@link
* #isDirect isDirect} method. </p>
*
+*
+ *
+ *
* <p> Methods in this class that do not otherwise have a value to return are
* specified to return the buffer upon which they are invoked. This allows
* method invocations to be chained.
*
+ *
+ *
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
*/
public abstract class IntBuffer
- extends Buffer
- implements Comparable<IntBuffer> {
+ extends Buffer
+ implements Comparable<IntBuffer>
+{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
@@ -96,7 +107,8 @@ public abstract class IntBuffer
// backing array, and array offset
//
IntBuffer(int mark, int pos, int lim, int cap, // package-private
- int[] hb, int offset) {
+ int[] hb, int offset)
+ {
super(mark, pos, lim, cap, 2);
this.hb = hb;
this.offset = offset;
@@ -114,13 +126,16 @@ public abstract class IntBuffer
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
- * initialized to zero. It will have a {@link #array
- * </code>backing array<code>}, and its {@link #arrayOffset </code>array
- * offset<code>} will be zero.
+ * initialized to zero. It will have a {@link #array backing array},
+ * and its {@link #arrayOffset array offset} will be zero.
*
- * @param capacity The new buffer's capacity, in ints
- * @return The new int buffer
- * @throws IllegalArgumentException If the <tt>capacity</tt> is a negative integer
+ * @param capacity
+ * The new buffer's capacity, in ints
+ *
+ * @return The new int buffer
+ *
+ * @throws IllegalArgumentException
+ * If the <tt>capacity</tt> is a negative integer
*/
public static IntBuffer allocate(int capacity) {
if (capacity < 0)
@@ -136,24 +151,32 @@ public abstract class IntBuffer
* and vice versa. The new buffer's capacity will be
* <tt>array.length</tt>, its position will be <tt>offset</tt>, its limit
* will be <tt>offset + length</tt>, and its mark will be undefined. Its
- * {@link #array </code>backing array<code>} will be the given array, and
- * its {@link #arrayOffset </code>array offset<code>} will be zero. </p>
- *
- * @param array The array that will back the new buffer
- * @param offset The offset of the subarray to be used; must be non-negative and
- * no larger than <tt>array.length</tt>. The new buffer's position
- * will be set to this value.
- * @param length The length of the subarray to be used;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>.
- * The new buffer's limit will be set to <tt>offset + length</tt>.
- * @return The new int buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * {@link #array backing array} will be the given array, and
+ * its {@link #arrayOffset array offset} will be zero. </p>
+ *
+ * @param array
+ * The array that will back the new buffer
+ *
+ * @param offset
+ * The offset of the subarray to be used; must be non-negative and
+ * no larger than <tt>array.length</tt>. The new buffer's position
+ * will be set to this value.
+ *
+ * @param length
+ * The length of the subarray to be used;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>.
+ * The new buffer's limit will be set to <tt>offset + length</tt>.
+ *
+ * @return The new int buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public static IntBuffer wrap(int[] array,
- int offset, int length) {
+ int offset, int length)
+ {
try {
return new HeapIntBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
@@ -168,12 +191,14 @@ public abstract class IntBuffer
* that is, modifications to the buffer will cause the array to be modified
* and vice versa. The new buffer's capacity and limit will be
* <tt>array.length</tt>, its position will be zero, and its mark will be
- * undefined. Its {@link #array </code>backing array<code>} will be the
- * given array, and its {@link #arrayOffset </code>array offset<code>} will
+ * undefined. Its {@link #array backing array} will be the
+ * given array, and its {@link #arrayOffset array offset>} will
* be zero. </p>
*
- * @param array The array that will back this buffer
- * @return The new int buffer
+ * @param array
+ * The array that will back this buffer
+ *
+ * @return The new int buffer
*/
public static IntBuffer wrap(int[] array) {
return wrap(array, 0, array.length);
@@ -195,7 +220,7 @@ public abstract class IntBuffer
* buffer is direct, and it will be read-only if, and only if, this buffer
* is read-only. </p>
*
- * @return The new int buffer
+ * @return The new int buffer
*/
public abstract IntBuffer slice();
@@ -212,7 +237,7 @@ public abstract class IntBuffer
* and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return The new int buffer
+ * @return The new int buffer
*/
public abstract IntBuffer duplicate();
@@ -232,7 +257,7 @@ public abstract class IntBuffer
* <p> If this buffer is itself read-only then this method behaves in
* exactly the same way as the {@link #duplicate duplicate} method. </p>
*
- * @return The new, read-only int buffer
+ * @return The new, read-only int buffer
*/
public abstract IntBuffer asReadOnlyBuffer();
@@ -241,11 +266,12 @@ public abstract class IntBuffer
/**
* Relative <i>get</i> method. Reads the int at this buffer's
- * current position, and then increments the position. </p>
+ * current position, and then increments the position.
*
- * @return The int at the buffer's current position
- * @throws BufferUnderflowException If the buffer's current position is not smaller than its
- * limit
+ * @return The int at the buffer's current position
+ *
+ * @throws BufferUnderflowException
+ * If the buffer's current position is not smaller than its limit
*/
public abstract int get();
@@ -255,22 +281,31 @@ public abstract class IntBuffer
* <p> Writes the given int into this buffer at the current
* position, and then increments the position. </p>
*
- * @param i The int to be written
- * @return This buffer
- * @throws BufferOverflowException If this buffer's current position is not smaller than its
- * limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param i
+ * The int to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If this buffer's current position is not smaller than its limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract IntBuffer put(int i);
/**
* Absolute <i>get</i> method. Reads the int at the given
- * index. </p>
+ * index.
+ *
+ * @param index
+ * The index from which the int will be read
*
- * @param index The index from which the int will be read
- * @return The int at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
+ * @return The int at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
*/
public abstract int get(int index);
@@ -280,12 +315,20 @@ public abstract class IntBuffer
* <p> Writes the given int into this buffer at the given
* index. </p>
*
- * @param index The index at which the int will be written
- * @param i The int value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the int will be written
+ *
+ * @param i
+ * The int value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract IntBuffer put(int index, int i);
@@ -311,26 +354,36 @@ public abstract class IntBuffer
* <tt>src.get(dst,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst[i] = src.get(); </pre>
+ * dst[i] = src.get();
+ * }</pre>
*
* except that it first checks that there are sufficient ints in
- * this buffer and it is potentially much more efficient. </p>
- *
- * @param dst The array into which ints are to be written
- * @param offset The offset within the array of the first int to be
- * written; must be non-negative and no larger than
- * <tt>dst.length</tt>
- * @param length The maximum number of ints to be written to the given
- * array; must be non-negative and no larger than
- * <tt>dst.length - offset</tt>
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> ints
- * remaining in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * this buffer and it is potentially much more efficient.
+ *
+ * @param dst
+ * The array into which ints are to be written
+ *
+ * @param offset
+ * The offset within the array of the first int to be
+ * written; must be non-negative and no larger than
+ * <tt>dst.length</tt>
+ *
+ * @param length
+ * The maximum number of ints to be written to the given
+ * array; must be non-negative and no larger than
+ * <tt>dst.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> ints
+ * remaining in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public IntBuffer get(int[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
@@ -352,9 +405,14 @@ public abstract class IntBuffer
* <pre>
* src.get(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> ints
- * remaining in this buffer
+ * @param dst
+ * The destination array
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> ints
+ * remaining in this buffer
*/
public IntBuffer get(int[] dst) {
return get(dst, 0, dst.length);
@@ -386,15 +444,23 @@ public abstract class IntBuffer
* dst.put(src.get()); </pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The source buffer from which ints are to be read;
- * must not be this buffer
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * for the remaining ints in the source buffer
- * @throws IllegalArgumentException If the source buffer is this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The source buffer from which ints are to be read;
+ * must not be this buffer
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ * for the remaining ints in the source buffer
+ *
+ * @throws IllegalArgumentException
+ * If the source buffer is this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public IntBuffer put(IntBuffer src) {
if (src == this)
@@ -426,25 +492,37 @@ public abstract class IntBuffer
* <tt>dst.put(src,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst.put(a[i]); </pre>
+ * dst.put(a[i]);
+ * }</pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The array from which ints are to be read
- * @param offset The offset within the array of the first int to be read;
- * must be non-negative and no larger than <tt>array.length</tt>
- * @param length The number of ints to be read from the given array;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The array from which ints are to be read
+ *
+ * @param offset
+ * The offset within the array of the first int to be read;
+ * must be non-negative and no larger than <tt>array.length</tt>
+ *
+ * @param length
+ * The number of ints to be read from the given array;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public IntBuffer put(int[] src, int offset, int length) {
checkBounds(offset, length, src.length);
@@ -467,9 +545,16 @@ public abstract class IntBuffer
* <pre>
* dst.put(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param src
+ * The source array
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public final IntBuffer put(int[] src) {
return put(src, 0, src.length);
@@ -486,8 +571,8 @@ public abstract class IntBuffer
* and {@link #arrayOffset() arrayOffset} methods may safely be invoked.
* </p>
*
- * @return <tt>true</tt> if, and only if, this buffer
- * is backed by an array and is not read-only
+ * @return <tt>true</tt> if, and only if, this buffer
+ * is backed by an array and is not read-only
*/
public final boolean hasArray() {
return (hb != null) && !isReadOnly;
@@ -504,9 +589,13 @@ public abstract class IntBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The array that backs this buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The array that backs this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final int[] array() {
if (hb == null)
@@ -527,10 +616,14 @@ public abstract class IntBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The offset within this buffer's array
- * of the first element of the buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The offset within this buffer's array
+ * of the first element of the buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final int arrayOffset() {
if (hb == null)
@@ -558,23 +651,27 @@ public abstract class IntBuffer
* followed immediately by an invocation of another relative <i>put</i>
* method. </p>
*
- * @return This buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+
+ *
+ * @return This buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract IntBuffer compact();
/**
- * Tells whether or not this int buffer is direct. </p>
+ * Tells whether or not this int buffer is direct.
*
- * @return <tt>true</tt> if, and only if, this buffer is direct
+ * @return <tt>true</tt> if, and only if, this buffer is direct
*/
public abstract boolean isDirect();
/**
- * Returns a string summarizing the state of this buffer. </p>
+ * Returns a string summarizing the state of this buffer.
*
- * @return A summary string
+ * @return A summary string
*/
public String toString() {
StringBuffer sb = new StringBuffer();
@@ -601,7 +698,7 @@ public abstract class IntBuffer
* to use buffers as keys in hash maps or similar data structures unless it
* is known that their contents will not change. </p>
*
- * @return The current hash code of this buffer
+ * @return The current hash code of this buffer
*/
public int hashCode() {
int h = 1;
@@ -616,15 +713,15 @@ public abstract class IntBuffer
*
* <p> Two int buffers are equal if, and only if,
*
- * <p><ol>
+ * <ol>
*
- * <li><p> They have the same element type, </p></li>
+ * <li><p> They have the same element type, </p></li>
*
- * <li><p> They have the same number of remaining elements, and
- * </p></li>
+ * <li><p> They have the same number of remaining elements, and
+ * </p></li>
*
- * <li><p> The two sequences of remaining elements, considered
- * independently of their starting positions, are pointwise equal.
+ * <li><p> The two sequences of remaining elements, considered
+ * independently of their starting positions, are pointwise equal.
*
*
*
@@ -632,22 +729,23 @@ public abstract class IntBuffer
*
*
*
- * </p></li>
+ * </p></li>
*
* </ol>
*
* <p> A int buffer is not equal to any other type of object. </p>
*
- * @param ob The object to which this buffer is to be compared
- * @return <tt>true</tt> if, and only if, this buffer is equal to the
- * given object
+ * @param ob The object to which this buffer is to be compared
+ *
+ * @return <tt>true</tt> if, and only if, this buffer is equal to the
+ * given object
*/
public boolean equals(Object ob) {
if (this == ob)
return true;
if (!(ob instanceof IntBuffer))
return false;
- IntBuffer that = (IntBuffer) ob;
+ IntBuffer that = (IntBuffer)ob;
if (this.remaining() != that.remaining())
return false;
int p = this.position();
@@ -679,13 +777,13 @@ public abstract class IntBuffer
*
*
* Pairs of {@code int} elements are compared as if by invoking
- * {@link Integer#compare(int, int)}.
- *
+ * {@link Integer#compare(int,int)}.
+
*
* <p> A int buffer is not comparable to any other type of object.
*
- * @return A negative integer, zero, or a positive integer as this buffer
- * is less than, equal to, or greater than the given buffer
+ * @return A negative integer, zero, or a positive integer as this buffer
+ * is less than, equal to, or greater than the given buffer
*/
public int compareTo(IntBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
@@ -715,12 +813,12 @@ public abstract class IntBuffer
*
* <p> The byte order of an int buffer created by allocation or by
* wrapping an existing <tt>int</tt> array is the {@link
- * ByteOrder#nativeOrder </code>native order<code>} of the underlying
+ * ByteOrder#nativeOrder native order} of the underlying
* hardware. The byte order of an int buffer created as a <a
* href="ByteBuffer.html#views">view</a> of a byte buffer is that of the
* byte buffer at the moment that the view is created. </p>
*
- * @return This buffer's byte order
+ * @return This buffer's byte order
*/
public abstract ByteOrder order();
diff --git a/java/nio/InvalidMarkException.java b/java/nio/InvalidMarkException.java
index 328c02cd..ed1a059f 100644
--- a/java/nio/InvalidMarkException.java
+++ b/java/nio/InvalidMarkException.java
@@ -38,14 +38,14 @@ package java.nio;
*/
public class InvalidMarkException
- extends IllegalStateException {
+ extends IllegalStateException
+{
private static final long serialVersionUID = 1698329710438510774L;
/**
* Constructs an instance of this class.
*/
- public InvalidMarkException() {
- }
+ public InvalidMarkException() { }
}
diff --git a/java/nio/LongBuffer.java b/java/nio/LongBuffer.java
index c1c2888b..80e506cf 100644
--- a/java/nio/LongBuffer.java
+++ b/java/nio/LongBuffer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2000, 2008, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -36,31 +36,37 @@ package java.nio;
*
* <ul>
*
- * <li><p> Absolute and relative {@link #get() </code><i>get</i><code>} and
- * {@link #put(long) </code><i>put</i><code>} methods that read and write
- * single longs; </p></li>
+ * <li><p> Absolute and relative {@link #get() <i>get</i>} and
+ * {@link #put(long) <i>put</i>} methods that read and write
+ * single longs; </p></li>
*
- * <li><p> Relative {@link #get(long[]) </code><i>bulk get</i><code>}
- * methods that transfer contiguous sequences of longs from this buffer
- * into an array; and</p></li>
+ * <li><p> Relative {@link #get(long[]) <i>bulk get</i>}
+ * methods that transfer contiguous sequences of longs from this buffer
+ * into an array; and</p></li>
*
- * <li><p> Relative {@link #put(long[]) </code><i>bulk put</i><code>}
- * methods that transfer contiguous sequences of longs from a
- * long array or some other long
- * buffer into this buffer;&#32;and </p></li>
+ * <li><p> Relative {@link #put(long[]) <i>bulk put</i>}
+ * methods that transfer contiguous sequences of longs from a
+ * long array or some other long
+ * buffer into this buffer;&#32;and </p></li>
*
- * <li><p> Methods for {@link #compact </code>compacting<code>}, {@link
- * #duplicate </code>duplicating<code>}, and {@link #slice
- * </code>slicing<code>} a long buffer. </p></li>
+ *
+ * <li><p> Methods for {@link #compact compacting}, {@link
+ * #duplicate duplicating}, and {@link #slice slicing}
+ * a long buffer. </p></li>
*
* </ul>
*
* <p> Long buffers can be created either by {@link #allocate
- * </code><i>allocation</i><code>}, which allocates space for the buffer's
- * content, by {@link #wrap(long[]) </code><i>wrapping</i><code>} an existing
+ * <i>allocation</i>}, which allocates space for the buffer's
+ *
+ *
+ * content, by {@link #wrap(long[]) <i>wrapping</i>} an existing
* long array into a buffer, or by creating a
* <a href="ByteBuffer.html#views"><i>view</i></a> of an existing byte buffer.
*
+ *
+*
+ *
* <p> Like a byte buffer, a long buffer is either <a
* href="ByteBuffer.html#direct"><i>direct</i> or <i>non-direct</i></a>. A
* long buffer created via the <tt>wrap</tt> methods of this class will
@@ -69,18 +75,24 @@ package java.nio;
* a long buffer is direct may be determined by invoking the {@link
* #isDirect isDirect} method. </p>
*
+*
+ *
+ *
* <p> Methods in this class that do not otherwise have a value to return are
* specified to return the buffer upon which they are invoked. This allows
* method invocations to be chained.
*
+ *
+ *
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
*/
public abstract class LongBuffer
- extends Buffer
- implements Comparable<LongBuffer> {
+ extends Buffer
+ implements Comparable<LongBuffer>
+{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
@@ -94,7 +106,8 @@ public abstract class LongBuffer
// backing array, and array offset
//
LongBuffer(int mark, int pos, int lim, int cap, // package-private
- long[] hb, int offset) {
+ long[] hb, int offset)
+ {
super(mark, pos, lim, cap, 3);
this.hb = hb;
this.offset = offset;
@@ -112,13 +125,16 @@ public abstract class LongBuffer
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
- * initialized to zero. It will have a {@link #array
- * </code>backing array<code>}, and its {@link #arrayOffset </code>array
- * offset<code>} will be zero.
+ * initialized to zero. It will have a {@link #array backing array},
+ * and its {@link #arrayOffset array offset} will be zero.
*
- * @param capacity The new buffer's capacity, in longs
- * @return The new long buffer
- * @throws IllegalArgumentException If the <tt>capacity</tt> is a negative integer
+ * @param capacity
+ * The new buffer's capacity, in longs
+ *
+ * @return The new long buffer
+ *
+ * @throws IllegalArgumentException
+ * If the <tt>capacity</tt> is a negative integer
*/
public static LongBuffer allocate(int capacity) {
if (capacity < 0)
@@ -134,24 +150,32 @@ public abstract class LongBuffer
* and vice versa. The new buffer's capacity will be
* <tt>array.length</tt>, its position will be <tt>offset</tt>, its limit
* will be <tt>offset + length</tt>, and its mark will be undefined. Its
- * {@link #array </code>backing array<code>} will be the given array, and
- * its {@link #arrayOffset </code>array offset<code>} will be zero. </p>
- *
- * @param array The array that will back the new buffer
- * @param offset The offset of the subarray to be used; must be non-negative and
- * no larger than <tt>array.length</tt>. The new buffer's position
- * will be set to this value.
- * @param length The length of the subarray to be used;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>.
- * The new buffer's limit will be set to <tt>offset + length</tt>.
- * @return The new long buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * {@link #array backing array} will be the given array, and
+ * its {@link #arrayOffset array offset} will be zero. </p>
+ *
+ * @param array
+ * The array that will back the new buffer
+ *
+ * @param offset
+ * The offset of the subarray to be used; must be non-negative and
+ * no larger than <tt>array.length</tt>. The new buffer's position
+ * will be set to this value.
+ *
+ * @param length
+ * The length of the subarray to be used;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>.
+ * The new buffer's limit will be set to <tt>offset + length</tt>.
+ *
+ * @return The new long buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public static LongBuffer wrap(long[] array,
- int offset, int length) {
+ int offset, int length)
+ {
try {
return new HeapLongBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
@@ -166,12 +190,14 @@ public abstract class LongBuffer
* that is, modifications to the buffer will cause the array to be modified
* and vice versa. The new buffer's capacity and limit will be
* <tt>array.length</tt>, its position will be zero, and its mark will be
- * undefined. Its {@link #array </code>backing array<code>} will be the
- * given array, and its {@link #arrayOffset </code>array offset<code>} will
+ * undefined. Its {@link #array backing array} will be the
+ * given array, and its {@link #arrayOffset array offset>} will
* be zero. </p>
*
- * @param array The array that will back this buffer
- * @return The new long buffer
+ * @param array
+ * The array that will back this buffer
+ *
+ * @return The new long buffer
*/
public static LongBuffer wrap(long[] array) {
return wrap(array, 0, array.length);
@@ -193,7 +219,7 @@ public abstract class LongBuffer
* buffer is direct, and it will be read-only if, and only if, this buffer
* is read-only. </p>
*
- * @return The new long buffer
+ * @return The new long buffer
*/
public abstract LongBuffer slice();
@@ -210,7 +236,7 @@ public abstract class LongBuffer
* and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return The new long buffer
+ * @return The new long buffer
*/
public abstract LongBuffer duplicate();
@@ -230,7 +256,7 @@ public abstract class LongBuffer
* <p> If this buffer is itself read-only then this method behaves in
* exactly the same way as the {@link #duplicate duplicate} method. </p>
*
- * @return The new, read-only long buffer
+ * @return The new, read-only long buffer
*/
public abstract LongBuffer asReadOnlyBuffer();
@@ -239,11 +265,12 @@ public abstract class LongBuffer
/**
* Relative <i>get</i> method. Reads the long at this buffer's
- * current position, and then increments the position. </p>
+ * current position, and then increments the position.
+ *
+ * @return The long at the buffer's current position
*
- * @return The long at the buffer's current position
- * @throws BufferUnderflowException If the buffer's current position is not smaller than its
- * limit
+ * @throws BufferUnderflowException
+ * If the buffer's current position is not smaller than its limit
*/
public abstract long get();
@@ -253,22 +280,31 @@ public abstract class LongBuffer
* <p> Writes the given long into this buffer at the current
* position, and then increments the position. </p>
*
- * @param l The long to be written
- * @return This buffer
- * @throws BufferOverflowException If this buffer's current position is not smaller than its
- * limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param l
+ * The long to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If this buffer's current position is not smaller than its limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract LongBuffer put(long l);
/**
* Absolute <i>get</i> method. Reads the long at the given
- * index. </p>
+ * index.
+ *
+ * @param index
+ * The index from which the long will be read
*
- * @param index The index from which the long will be read
- * @return The long at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
+ * @return The long at the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
*/
public abstract long get(int index);
@@ -278,12 +314,20 @@ public abstract class LongBuffer
* <p> Writes the given long into this buffer at the given
* index. </p>
*
- * @param index The index at which the long will be written
- * @param l The long value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the long will be written
+ *
+ * @param l
+ * The long value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract LongBuffer put(int index, long l);
@@ -309,26 +353,36 @@ public abstract class LongBuffer
* <tt>src.get(dst,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst[i] = src.get(); </pre>
+ * dst[i] = src.get();
+ * }</pre>
*
* except that it first checks that there are sufficient longs in
- * this buffer and it is potentially much more efficient. </p>
- *
- * @param dst The array into which longs are to be written
- * @param offset The offset within the array of the first long to be
- * written; must be non-negative and no larger than
- * <tt>dst.length</tt>
- * @param length The maximum number of longs to be written to the given
- * array; must be non-negative and no larger than
- * <tt>dst.length - offset</tt>
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> longs
- * remaining in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * this buffer and it is potentially much more efficient.
+ *
+ * @param dst
+ * The array into which longs are to be written
+ *
+ * @param offset
+ * The offset within the array of the first long to be
+ * written; must be non-negative and no larger than
+ * <tt>dst.length</tt>
+ *
+ * @param length
+ * The maximum number of longs to be written to the given
+ * array; must be non-negative and no larger than
+ * <tt>dst.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> longs
+ * remaining in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public LongBuffer get(long[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
@@ -350,9 +404,14 @@ public abstract class LongBuffer
* <pre>
* src.get(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> longs
- * remaining in this buffer
+ * @param dst
+ * The destination array
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> longs
+ * remaining in this buffer
*/
public LongBuffer get(long[] dst) {
return get(dst, 0, dst.length);
@@ -384,15 +443,23 @@ public abstract class LongBuffer
* dst.put(src.get()); </pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The source buffer from which longs are to be read;
- * must not be this buffer
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * for the remaining longs in the source buffer
- * @throws IllegalArgumentException If the source buffer is this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The source buffer from which longs are to be read;
+ * must not be this buffer
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ * for the remaining longs in the source buffer
+ *
+ * @throws IllegalArgumentException
+ * If the source buffer is this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public LongBuffer put(LongBuffer src) {
if (src == this)
@@ -424,25 +491,37 @@ public abstract class LongBuffer
* <tt>dst.put(src,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst.put(a[i]); </pre>
+ * dst.put(a[i]);
+ * }</pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The array from which longs are to be read
- * @param offset The offset within the array of the first long to be read;
- * must be non-negative and no larger than <tt>array.length</tt>
- * @param length The number of longs to be read from the given array;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The array from which longs are to be read
+ *
+ * @param offset
+ * The offset within the array of the first long to be read;
+ * must be non-negative and no larger than <tt>array.length</tt>
+ *
+ * @param length
+ * The number of longs to be read from the given array;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public LongBuffer put(long[] src, int offset, int length) {
checkBounds(offset, length, src.length);
@@ -465,9 +544,16 @@ public abstract class LongBuffer
* <pre>
* dst.put(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param src
+ * The source array
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public final LongBuffer put(long[] src) {
return put(src, 0, src.length);
@@ -484,8 +570,8 @@ public abstract class LongBuffer
* and {@link #arrayOffset() arrayOffset} methods may safely be invoked.
* </p>
*
- * @return <tt>true</tt> if, and only if, this buffer
- * is backed by an array and is not read-only
+ * @return <tt>true</tt> if, and only if, this buffer
+ * is backed by an array and is not read-only
*/
public final boolean hasArray() {
return (hb != null) && !isReadOnly;
@@ -502,9 +588,13 @@ public abstract class LongBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The array that backs this buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The array that backs this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final long[] array() {
if (hb == null)
@@ -525,10 +615,14 @@ public abstract class LongBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The offset within this buffer's array
- * of the first element of the buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The offset within this buffer's array
+ * of the first element of the buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final int arrayOffset() {
if (hb == null)
@@ -556,23 +650,27 @@ public abstract class LongBuffer
* followed immediately by an invocation of another relative <i>put</i>
* method. </p>
*
- * @return This buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+
+ *
+ * @return This buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract LongBuffer compact();
/**
- * Tells whether or not this long buffer is direct. </p>
+ * Tells whether or not this long buffer is direct.
*
- * @return <tt>true</tt> if, and only if, this buffer is direct
+ * @return <tt>true</tt> if, and only if, this buffer is direct
*/
public abstract boolean isDirect();
/**
- * Returns a string summarizing the state of this buffer. </p>
+ * Returns a string summarizing the state of this buffer.
*
- * @return A summary string
+ * @return A summary string
*/
public String toString() {
StringBuffer sb = new StringBuffer();
@@ -599,7 +697,7 @@ public abstract class LongBuffer
* to use buffers as keys in hash maps or similar data structures unless it
* is known that their contents will not change. </p>
*
- * @return The current hash code of this buffer
+ * @return The current hash code of this buffer
*/
public int hashCode() {
int h = 1;
@@ -614,38 +712,33 @@ public abstract class LongBuffer
*
* <p> Two long buffers are equal if, and only if,
*
- * <p><ol>
- *
- * <li><p> They have the same element type, </p></li>
- *
- * <li><p> They have the same number of remaining elements, and
- * </p></li>
- *
- * <li><p> The two sequences of remaining elements, considered
- * independently of their starting positions, are pointwise equal.
+ * <ol>
*
+ * <li><p> They have the same element type, </p></li>
*
+ * <li><p> They have the same number of remaining elements, and
+ * </p></li>
*
- *
- *
- *
- *
- * </p></li>
+ * <li><p> The two sequences of remaining elements, considered
+ * independently of their starting positions, are pointwise equal.
+
+ * </p></li>
*
* </ol>
*
* <p> A long buffer is not equal to any other type of object. </p>
*
- * @param ob The object to which this buffer is to be compared
- * @return <tt>true</tt> if, and only if, this buffer is equal to the
- * given object
+ * @param ob The object to which this buffer is to be compared
+ *
+ * @return <tt>true</tt> if, and only if, this buffer is equal to the
+ * given object
*/
public boolean equals(Object ob) {
if (this == ob)
return true;
if (!(ob instanceof LongBuffer))
return false;
- LongBuffer that = (LongBuffer) ob;
+ LongBuffer that = (LongBuffer)ob;
if (this.remaining() != that.remaining())
return false;
int p = this.position();
@@ -677,13 +770,13 @@ public abstract class LongBuffer
*
*
* Pairs of {@code long} elements are compared as if by invoking
- * {@link Long#compare(long, long)}.
- *
+ * {@link Long#compare(long,long)}.
+
*
* <p> A long buffer is not comparable to any other type of object.
*
- * @return A negative integer, zero, or a positive integer as this buffer
- * is less than, equal to, or greater than the given buffer
+ * @return A negative integer, zero, or a positive integer as this buffer
+ * is less than, equal to, or greater than the given buffer
*/
public int compareTo(LongBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
@@ -713,12 +806,12 @@ public abstract class LongBuffer
*
* <p> The byte order of a long buffer created by allocation or by
* wrapping an existing <tt>long</tt> array is the {@link
- * ByteOrder#nativeOrder </code>native order<code>} of the underlying
+ * ByteOrder#nativeOrder native order} of the underlying
* hardware. The byte order of a long buffer created as a <a
* href="ByteBuffer.html#views">view</a> of a byte buffer is that of the
* byte buffer at the moment that the view is created. </p>
*
- * @return This buffer's byte order
+ * @return This buffer's byte order
*/
public abstract ByteOrder order();
diff --git a/java/nio/MappedByteBuffer.java b/java/nio/MappedByteBuffer.java
index e85f3a26..2ea6cbd5 100644
--- a/java/nio/MappedByteBuffer.java
+++ b/java/nio/MappedByteBuffer.java
@@ -27,7 +27,6 @@
package java.nio;
import java.io.FileDescriptor;
-
import sun.misc.Unsafe;
@@ -59,13 +58,15 @@ import sun.misc.Unsafe;
* <p> Mapped byte buffers otherwise behave no differently than ordinary direct
* byte buffers. </p>
*
+ *
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
*/
public abstract class MappedByteBuffer
- extends ByteBuffer {
+ extends ByteBuffer
+{
// This is a little bit backwards: By rights MappedByteBuffer should be a
// subclass of DirectByteBuffer, but to keep the spec clear and simple, and
@@ -79,11 +80,13 @@ public abstract class MappedByteBuffer
// This should only be invoked by the DirectByteBuffer constructors
//
MappedByteBuffer(int mark, int pos, int lim, int cap, // package-private
- FileDescriptor fd) {
+ FileDescriptor fd)
+ {
super(mark, pos, lim, cap);
this.fd = fd;
}
+ // Android-added: Additional constructor for use by Android's DirectByteBuffer.
MappedByteBuffer(int mark, int pos, int lim, int cap, byte[] buf, int offset) {
super(mark, pos, lim, cap, buf, offset);
this.fd = null;
@@ -113,7 +116,7 @@ public abstract class MappedByteBuffer
}
private long mappingLength(long mappingOffset) {
- return (long) capacity() + mappingOffset;
+ return (long)capacity() + mappingOffset;
}
/**
@@ -131,8 +134,8 @@ public abstract class MappedByteBuffer
* underlying operating system may have paged out some of the buffer's data
* by the time that an invocation of this method returns. </p>
*
- * @return <tt>true</tt> if it is likely that this buffer's content
- * is resident in physical memory
+ * @return <tt>true</tt> if it is likely that this buffer's content
+ * is resident in physical memory
*/
public final boolean isLoaded() {
checkMapped();
@@ -154,7 +157,7 @@ public abstract class MappedByteBuffer
* method may cause some number of page faults and I/O operations to
* occur. </p>
*
- * @return This buffer
+ * @return This buffer
*/
public final MappedByteBuffer load() {
checkMapped();
@@ -172,7 +175,7 @@ public abstract class MappedByteBuffer
int count = Bits.pageCount(length);
long a = mappingAddress(offset);
byte x = 0;
- for (int i = 0; i < count; i++) {
+ for (int i=0; i<count; i++) {
x ^= unsafe.getByte(a);
a += ps;
}
@@ -198,7 +201,7 @@ public abstract class MappedByteBuffer
* java.nio.channels.FileChannel.MapMode#READ_WRITE}) then invoking this
* method has no effect. </p>
*
- * @return This buffer
+ * @return This buffer
*/
public final MappedByteBuffer force() {
checkMapped();
@@ -210,8 +213,6 @@ public abstract class MappedByteBuffer
}
private native boolean isLoaded0(long address, long length, int pageCount);
-
private native void load0(long address, long length);
-
private native void force0(FileDescriptor fd, long address, long length);
}
diff --git a/java/nio/ShortBuffer.java b/java/nio/ShortBuffer.java
index 9ca0f369..6903b542 100644
--- a/java/nio/ShortBuffer.java
+++ b/java/nio/ShortBuffer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2000, 2008, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -36,32 +36,37 @@ package java.nio;
*
* <ul>
*
- * <li><p> Absolute and relative {@link #get() </code><i>get</i><code>} and
- * {@link #put(short) </code><i>put</i><code>} methods that read and write
- * single shorts; </p></li>
+ * <li><p> Absolute and relative {@link #get() <i>get</i>} and
+ * {@link #put(short) <i>put</i>} methods that read and write
+ * single shorts; </p></li>
*
- * <li><p> Relative {@link #get(short[]) </code><i>bulk get</i><code>}
- * methods that transfer contiguous sequences of shorts from this buffer
- * into an array; and</p></li>
+ * <li><p> Relative {@link #get(short[]) <i>bulk get</i>}
+ * methods that transfer contiguous sequences of shorts from this buffer
+ * into an array; and</p></li>
*
- * <li><p> Relative {@link #put(short[]) </code><i>bulk put</i><code>}
- * methods that transfer contiguous sequences of shorts from a
- * short array or some other short
- * buffer into this buffer;&#32;and </p></li>
+ * <li><p> Relative {@link #put(short[]) <i>bulk put</i>}
+ * methods that transfer contiguous sequences of shorts from a
+ * short array or some other short
+ * buffer into this buffer;&#32;and </p></li>
*
- * <li><p> Methods for {@link #compact </code>compacting<code>}, {@link
- * #duplicate </code>duplicating<code>}, and {@link #slice
- * </code>slicing<code>} a short buffer. </p></li>
+ *
+ * <li><p> Methods for {@link #compact compacting}, {@link
+ * #duplicate duplicating}, and {@link #slice slicing}
+ * a short buffer. </p></li>
*
* </ul>
*
* <p> Short buffers can be created either by {@link #allocate
- * </code><i>allocation</i><code>}, which allocates space for the buffer's
+ * <i>allocation</i>}, which allocates space for the buffer's
+ *
*
- * content, by {@link #wrap(short[]) </code><i>wrapping</i><code>} an existing
+ * content, by {@link #wrap(short[]) <i>wrapping</i>} an existing
* short array into a buffer, or by creating a
* <a href="ByteBuffer.html#views"><i>view</i></a> of an existing byte buffer.
*
+ *
+*
+ *
* <p> Like a byte buffer, a short buffer is either <a
* href="ByteBuffer.html#direct"><i>direct</i> or <i>non-direct</i></a>. A
* short buffer created via the <tt>wrap</tt> methods of this class will
@@ -70,18 +75,24 @@ package java.nio;
* a short buffer is direct may be determined by invoking the {@link
* #isDirect isDirect} method. </p>
*
+*
+ *
+ *
* <p> Methods in this class that do not otherwise have a value to return are
* specified to return the buffer upon which they are invoked. This allows
* method invocations to be chained.
*
+ *
+ *
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
*/
public abstract class ShortBuffer
- extends Buffer
- implements Comparable<ShortBuffer> {
+ extends Buffer
+ implements Comparable<ShortBuffer>
+{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
@@ -95,7 +106,8 @@ public abstract class ShortBuffer
// backing array, and array offset
//
ShortBuffer(int mark, int pos, int lim, int cap, // package-private
- short[] hb, int offset) {
+ short[] hb, int offset)
+ {
super(mark, pos, lim, cap, 1);
this.hb = hb;
this.offset = offset;
@@ -113,13 +125,16 @@ public abstract class ShortBuffer
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
- * initialized to zero. It will have a {@link #array
- * </code>backing array<code>}, and its {@link #arrayOffset </code>array
- * offset<code>} will be zero.
+ * initialized to zero. It will have a {@link #array backing array},
+ * and its {@link #arrayOffset array offset} will be zero.
+ *
+ * @param capacity
+ * The new buffer's capacity, in shorts
*
- * @param capacity The new buffer's capacity, in shorts
- * @return The new short buffer
- * @throws IllegalArgumentException If the <tt>capacity</tt> is a negative integer
+ * @return The new short buffer
+ *
+ * @throws IllegalArgumentException
+ * If the <tt>capacity</tt> is a negative integer
*/
public static ShortBuffer allocate(int capacity) {
if (capacity < 0)
@@ -135,24 +150,32 @@ public abstract class ShortBuffer
* and vice versa. The new buffer's capacity will be
* <tt>array.length</tt>, its position will be <tt>offset</tt>, its limit
* will be <tt>offset + length</tt>, and its mark will be undefined. Its
- * {@link #array </code>backing array<code>} will be the given array, and
- * its {@link #arrayOffset </code>array offset<code>} will be zero. </p>
- *
- * @param array The array that will back the new buffer
- * @param offset The offset of the subarray to be used; must be non-negative and
- * no larger than <tt>array.length</tt>. The new buffer's position
- * will be set to this value.
- * @param length The length of the subarray to be used;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>.
- * The new buffer's limit will be set to <tt>offset + length</tt>.
- * @return The new short buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * {@link #array backing array} will be the given array, and
+ * its {@link #arrayOffset array offset} will be zero. </p>
+ *
+ * @param array
+ * The array that will back the new buffer
+ *
+ * @param offset
+ * The offset of the subarray to be used; must be non-negative and
+ * no larger than <tt>array.length</tt>. The new buffer's position
+ * will be set to this value.
+ *
+ * @param length
+ * The length of the subarray to be used;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>.
+ * The new buffer's limit will be set to <tt>offset + length</tt>.
+ *
+ * @return The new short buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public static ShortBuffer wrap(short[] array,
- int offset, int length) {
+ int offset, int length)
+ {
try {
return new HeapShortBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
@@ -167,12 +190,14 @@ public abstract class ShortBuffer
* that is, modifications to the buffer will cause the array to be modified
* and vice versa. The new buffer's capacity and limit will be
* <tt>array.length</tt>, its position will be zero, and its mark will be
- * undefined. Its {@link #array </code>backing array<code>} will be the
- * given array, and its {@link #arrayOffset </code>array offset<code>} will
+ * undefined. Its {@link #array backing array} will be the
+ * given array, and its {@link #arrayOffset array offset>} will
* be zero. </p>
*
- * @param array The array that will back this buffer
- * @return The new short buffer
+ * @param array
+ * The array that will back this buffer
+ *
+ * @return The new short buffer
*/
public static ShortBuffer wrap(short[] array) {
return wrap(array, 0, array.length);
@@ -194,7 +219,7 @@ public abstract class ShortBuffer
* buffer is direct, and it will be read-only if, and only if, this buffer
* is read-only. </p>
*
- * @return The new short buffer
+ * @return The new short buffer
*/
public abstract ShortBuffer slice();
@@ -211,7 +236,7 @@ public abstract class ShortBuffer
* and only if, this buffer is direct, and it will be read-only if, and
* only if, this buffer is read-only. </p>
*
- * @return The new short buffer
+ * @return The new short buffer
*/
public abstract ShortBuffer duplicate();
@@ -231,7 +256,7 @@ public abstract class ShortBuffer
* <p> If this buffer is itself read-only then this method behaves in
* exactly the same way as the {@link #duplicate duplicate} method. </p>
*
- * @return The new, read-only short buffer
+ * @return The new, read-only short buffer
*/
public abstract ShortBuffer asReadOnlyBuffer();
@@ -240,11 +265,12 @@ public abstract class ShortBuffer
/**
* Relative <i>get</i> method. Reads the short at this buffer's
- * current position, and then increments the position. </p>
+ * current position, and then increments the position.
*
- * @return The short at the buffer's current position
- * @throws BufferUnderflowException If the buffer's current position is not smaller than its
- * limit
+ * @return The short at the buffer's current position
+ *
+ * @throws BufferUnderflowException
+ * If the buffer's current position is not smaller than its limit
*/
public abstract short get();
@@ -254,22 +280,31 @@ public abstract class ShortBuffer
* <p> Writes the given short into this buffer at the current
* position, and then increments the position. </p>
*
- * @param s The short to be written
- * @return This buffer
- * @throws BufferOverflowException If this buffer's current position is not smaller than its
- * limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param s
+ * The short to be written
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If this buffer's current position is not smaller than its limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ShortBuffer put(short s);
/**
* Absolute <i>get</i> method. Reads the short at the given
- * index. </p>
+ * index.
+ *
+ * @param index
+ * The index from which the short will be read
+ *
+ * @return The short at the given index
*
- * @param index The index from which the short will be read
- * @return The short at the given index
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
*/
public abstract short get(int index);
@@ -279,12 +314,20 @@ public abstract class ShortBuffer
* <p> Writes the given short into this buffer at the given
* index. </p>
*
- * @param index The index at which the short will be written
- * @param s The short value to be written
- * @return This buffer
- * @throws IndexOutOfBoundsException If <tt>index</tt> is negative
- * or not smaller than the buffer's limit
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param index
+ * The index at which the short will be written
+ *
+ * @param s
+ * The short value to be written
+ *
+ * @return This buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If <tt>index</tt> is negative
+ * or not smaller than the buffer's limit
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ShortBuffer put(int index, short s);
@@ -310,26 +353,36 @@ public abstract class ShortBuffer
* <tt>src.get(dst,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst[i] = src.get(); </pre>
+ * dst[i] = src.get();
+ * }</pre>
*
* except that it first checks that there are sufficient shorts in
- * this buffer and it is potentially much more efficient. </p>
- *
- * @param dst The array into which shorts are to be written
- * @param offset The offset within the array of the first short to be
- * written; must be non-negative and no larger than
- * <tt>dst.length</tt>
- * @param length The maximum number of shorts to be written to the given
- * array; must be non-negative and no larger than
- * <tt>dst.length - offset</tt>
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> shorts
- * remaining in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
+ * this buffer and it is potentially much more efficient.
+ *
+ * @param dst
+ * The array into which shorts are to be written
+ *
+ * @param offset
+ * The offset within the array of the first short to be
+ * written; must be non-negative and no larger than
+ * <tt>dst.length</tt>
+ *
+ * @param length
+ * The maximum number of shorts to be written to the given
+ * array; must be non-negative and no larger than
+ * <tt>dst.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> shorts
+ * remaining in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
*/
public ShortBuffer get(short[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
@@ -351,9 +404,14 @@ public abstract class ShortBuffer
* <pre>
* src.get(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferUnderflowException If there are fewer than <tt>length</tt> shorts
- * remaining in this buffer
+ * @param dst
+ * The destination array
+ *
+ * @return This buffer
+ *
+ * @throws BufferUnderflowException
+ * If there are fewer than <tt>length</tt> shorts
+ * remaining in this buffer
*/
public ShortBuffer get(short[] dst) {
return get(dst, 0, dst.length);
@@ -385,15 +443,23 @@ public abstract class ShortBuffer
* dst.put(src.get()); </pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The source buffer from which shorts are to be read;
- * must not be this buffer
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * for the remaining shorts in the source buffer
- * @throws IllegalArgumentException If the source buffer is this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The source buffer from which shorts are to be read;
+ * must not be this buffer
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ * for the remaining shorts in the source buffer
+ *
+ * @throws IllegalArgumentException
+ * If the source buffer is this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public ShortBuffer put(ShortBuffer src) {
if (src == this)
@@ -425,25 +491,37 @@ public abstract class ShortBuffer
* <tt>dst.put(src,&nbsp;off,&nbsp;len)</tt> has exactly the same effect as
* the loop
*
- * <pre>
+ * <pre>{@code
* for (int i = off; i < off + len; i++)
- * dst.put(a[i]); </pre>
+ * dst.put(a[i]);
+ * }</pre>
*
* except that it first checks that there is sufficient space in this
- * buffer and it is potentially much more efficient. </p>
- *
- * @param src The array from which shorts are to be read
- * @param offset The offset within the array of the first short to be read;
- * must be non-negative and no larger than <tt>array.length</tt>
- * @param length The number of shorts to be read from the given array;
- * must be non-negative and no larger than
- * <tt>array.length - offset</tt>
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws IndexOutOfBoundsException If the preconditions on the <tt>offset</tt> and
- * <tt>length</tt>
- * parameters do not hold
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * buffer and it is potentially much more efficient.
+ *
+ * @param src
+ * The array from which shorts are to be read
+ *
+ * @param offset
+ * The offset within the array of the first short to be read;
+ * must be non-negative and no larger than <tt>array.length</tt>
+ *
+ * @param length
+ * The number of shorts to be read from the given array;
+ * must be non-negative and no larger than
+ * <tt>array.length - offset</tt>
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws IndexOutOfBoundsException
+ * If the preconditions on the <tt>offset</tt> and <tt>length</tt>
+ * parameters do not hold
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public ShortBuffer put(short[] src, int offset, int length) {
checkBounds(offset, length, src.length);
@@ -466,9 +544,16 @@ public abstract class ShortBuffer
* <pre>
* dst.put(a, 0, a.length) </pre>
*
- * @return This buffer
- * @throws BufferOverflowException If there is insufficient space in this buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+ * @param src
+ * The source array
+ *
+ * @return This buffer
+ *
+ * @throws BufferOverflowException
+ * If there is insufficient space in this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public final ShortBuffer put(short[] src) {
return put(src, 0, src.length);
@@ -485,8 +570,8 @@ public abstract class ShortBuffer
* and {@link #arrayOffset() arrayOffset} methods may safely be invoked.
* </p>
*
- * @return <tt>true</tt> if, and only if, this buffer
- * is backed by an array and is not read-only
+ * @return <tt>true</tt> if, and only if, this buffer
+ * is backed by an array and is not read-only
*/
public final boolean hasArray() {
return (hb != null) && !isReadOnly;
@@ -503,9 +588,13 @@ public abstract class ShortBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The array that backs this buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The array that backs this buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final short[] array() {
if (hb == null)
@@ -526,10 +615,14 @@ public abstract class ShortBuffer
* method in order to ensure that this buffer has an accessible backing
* array. </p>
*
- * @return The offset within this buffer's array
- * of the first element of the buffer
- * @throws ReadOnlyBufferException If this buffer is backed by an array but is read-only
- * @throws UnsupportedOperationException If this buffer is not backed by an accessible array
+ * @return The offset within this buffer's array
+ * of the first element of the buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is backed by an array but is read-only
+ *
+ * @throws UnsupportedOperationException
+ * If this buffer is not backed by an accessible array
*/
public final int arrayOffset() {
if (hb == null)
@@ -557,23 +650,27 @@ public abstract class ShortBuffer
* followed immediately by an invocation of another relative <i>put</i>
* method. </p>
*
- * @return This buffer
- * @throws ReadOnlyBufferException If this buffer is read-only
+
+ *
+ * @return This buffer
+ *
+ * @throws ReadOnlyBufferException
+ * If this buffer is read-only
*/
public abstract ShortBuffer compact();
/**
- * Tells whether or not this short buffer is direct. </p>
+ * Tells whether or not this short buffer is direct.
*
- * @return <tt>true</tt> if, and only if, this buffer is direct
+ * @return <tt>true</tt> if, and only if, this buffer is direct
*/
public abstract boolean isDirect();
/**
- * Returns a string summarizing the state of this buffer. </p>
+ * Returns a string summarizing the state of this buffer.
*
- * @return A summary string
+ * @return A summary string
*/
public String toString() {
StringBuffer sb = new StringBuffer();
@@ -600,7 +697,7 @@ public abstract class ShortBuffer
* to use buffers as keys in hash maps or similar data structures unless it
* is known that their contents will not change. </p>
*
- * @return The current hash code of this buffer
+ * @return The current hash code of this buffer
*/
public int hashCode() {
int h = 1;
@@ -615,38 +712,33 @@ public abstract class ShortBuffer
*
* <p> Two short buffers are equal if, and only if,
*
- * <p><ol>
- *
- * <li><p> They have the same element type, </p></li>
- *
- * <li><p> They have the same number of remaining elements, and
- * </p></li>
- *
- * <li><p> The two sequences of remaining elements, considered
- * independently of their starting positions, are pointwise equal.
+ * <ol>
*
+ * <li><p> They have the same element type, </p></li>
*
+ * <li><p> They have the same number of remaining elements, and
+ * </p></li>
*
- *
- *
- *
- *
- * </p></li>
+ * <li><p> The two sequences of remaining elements, considered
+ * independently of their starting positions, are pointwise equal.
+
+ * </p></li>
*
* </ol>
*
* <p> A short buffer is not equal to any other type of object. </p>
*
- * @param ob The object to which this buffer is to be compared
- * @return <tt>true</tt> if, and only if, this buffer is equal to the
- * given object
+ * @param ob The object to which this buffer is to be compared
+ *
+ * @return <tt>true</tt> if, and only if, this buffer is equal to the
+ * given object
*/
public boolean equals(Object ob) {
if (this == ob)
return true;
if (!(ob instanceof ShortBuffer))
return false;
- ShortBuffer that = (ShortBuffer) ob;
+ ShortBuffer that = (ShortBuffer)ob;
if (this.remaining() != that.remaining())
return false;
int p = this.position();
@@ -678,13 +770,13 @@ public abstract class ShortBuffer
*
*
* Pairs of {@code short} elements are compared as if by invoking
- * {@link Short#compare(short, short)}.
- *
+ * {@link Short#compare(short,short)}.
+
*
* <p> A short buffer is not comparable to any other type of object.
*
- * @return A negative integer, zero, or a positive integer as this buffer
- * is less than, equal to, or greater than the given buffer
+ * @return A negative integer, zero, or a positive integer as this buffer
+ * is less than, equal to, or greater than the given buffer
*/
public int compareTo(ShortBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
@@ -714,12 +806,12 @@ public abstract class ShortBuffer
*
* <p> The byte order of a short buffer created by allocation or by
* wrapping an existing <tt>short</tt> array is the {@link
- * ByteOrder#nativeOrder </code>native order<code>} of the underlying
+ * ByteOrder#nativeOrder native order} of the underlying
* hardware. The byte order of a short buffer created as a <a
* href="ByteBuffer.html#views">view</a> of a byte buffer is that of the
* byte buffer at the moment that the view is created. </p>
*
- * @return This buffer's byte order
+ * @return This buffer's byte order
*/
public abstract ByteOrder order();
diff --git a/java/nio/StringCharBuffer.java b/java/nio/StringCharBuffer.java
index 6654c187..0c20fa66 100644
--- a/java/nio/StringCharBuffer.java
+++ b/java/nio/StringCharBuffer.java
@@ -29,7 +29,8 @@ package java.nio;
// ## If the sequence is a string, use reflection to share its array
class StringCharBuffer // package-private
- extends CharBuffer {
+ extends CharBuffer
+{
CharSequence str;
StringCharBuffer(CharSequence s, int start, int end) { // package-private
@@ -42,11 +43,11 @@ class StringCharBuffer // package-private
public CharBuffer slice() {
return new StringCharBuffer(str,
- -1,
- 0,
- this.remaining(),
- this.remaining(),
- offset + this.position());
+ -1,
+ 0,
+ this.remaining(),
+ this.remaining(),
+ offset + this.position());
}
private StringCharBuffer(CharSequence s,
@@ -61,7 +62,7 @@ class StringCharBuffer // package-private
public CharBuffer duplicate() {
return new StringCharBuffer(str, markValue(),
- position(), limit(), capacity(), offset);
+ position(), limit(), capacity(), offset);
}
public CharBuffer asReadOnlyBuffer() {
@@ -106,11 +107,11 @@ class StringCharBuffer // package-private
try {
int pos = position();
return new StringCharBuffer(str,
- -1,
- pos + checkIndex(start, pos),
- pos + checkIndex(end, pos),
- capacity(),
- offset);
+ -1,
+ pos + checkIndex(start, pos),
+ pos + checkIndex(end, pos),
+ capacity(),
+ offset);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
diff --git a/java/nio/channels/ServerSocketChannel.java b/java/nio/channels/ServerSocketChannel.java
index 7a5fd766..8d967b35 100644
--- a/java/nio/channels/ServerSocketChannel.java
+++ b/java/nio/channels/ServerSocketChannel.java
@@ -141,7 +141,7 @@ public abstract class ServerSocketChannel
* @return This channel
*
* @throws AlreadyBoundException {@inheritDoc}
- * @throws UnresolvedAddressException
+ * @throws UnsupportedAddressTypeException {@inheritDoc}
* @throws ClosedChannelException {@inheritDoc}
* @throws IOException {@inheritDoc}
* @throws SecurityException
diff --git a/java/nio/file/FileSystem.java b/java/nio/file/FileSystem.java
index bb6f1364..755d64fd 100644
--- a/java/nio/file/FileSystem.java
+++ b/java/nio/file/FileSystem.java
@@ -294,6 +294,7 @@ public abstract class FileSystem
*/
public abstract Path getPath(String first, String... more);
+ // Android-changed: Removed javadoc references to UNIX and Windows.
/**
* Returns a {@code PathMatcher} that performs match operations on the
* {@code String} representation of {@link Path} objects by interpreting a
diff --git a/java/text/SimpleDateFormat.java b/java/text/SimpleDateFormat.java
index dfbd1218..8ebe7c54 100644
--- a/java/text/SimpleDateFormat.java
+++ b/java/text/SimpleDateFormat.java
@@ -1339,7 +1339,7 @@ public class SimpleDateFormat extends DateFormat {
break;
case PATTERN_ZONE_VALUE: // 'Z' ("-/+hhmm" form)
- // BEGIN Android-Changed: use shared code in TimeZone for zone offset string.
+ // BEGIN Android-changed: use shared code in TimeZone for zone offset string.
{
value = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
final boolean includeSeparator = (count >= 4);
@@ -1348,7 +1348,7 @@ public class SimpleDateFormat extends DateFormat {
break;
}
- // END Android-Changed: use shared code in TimeZone for zone offset string.
+ // END Android-changed: use shared code in TimeZone for zone offset string.
case PATTERN_ISO_ZONE: // 'X'
value = calendar.get(Calendar.ZONE_OFFSET)
diff --git a/java/util/TimeZone.java b/java/util/TimeZone.java
index 6ad180b7..de4d4d14 100644
--- a/java/util/TimeZone.java
+++ b/java/util/TimeZone.java
@@ -743,7 +743,7 @@ abstract public class TimeZone implements Serializable, Cloneable {
}
defaultTimeZone = timeZone != null ? (TimeZone) timeZone.clone() : null;
// Android-changed: notify ICU4J of changed default TimeZone.
- android.icu.util.TimeZone.clearCachedDefault();
+ android.icu.util.TimeZone.setICUDefault(null);
}
/**
diff --git a/java/util/concurrent/ConcurrentHashMap.java b/java/util/concurrent/ConcurrentHashMap.java
index 239df12e..2b51d91b 100644
--- a/java/util/concurrent/ConcurrentHashMap.java
+++ b/java/util/concurrent/ConcurrentHashMap.java
@@ -1241,7 +1241,7 @@ public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
*
* @return the set view
*/
- // NOTE: The upstream version of this function returns KeySetView (See http://b/28099367).
+ // Android-changed: Return type for backwards compat. Was KeySetView<K,V>. http://b/28099367
public Set<K> keySet() {
KeySetView<K,V> ks;
return (ks = keySet) != null ? ks : (keySet = new KeySetView<K,V>(this, null));
diff --git a/java/util/concurrent/TimeUnit.java b/java/util/concurrent/TimeUnit.java
index fa520832..44d7964c 100644
--- a/java/util/concurrent/TimeUnit.java
+++ b/java/util/concurrent/TimeUnit.java
@@ -37,10 +37,6 @@ package java.util.concurrent;
import java.util.Objects;
-// BEGIN android-note
-// removed java 9 ChronoUnit related code
-// END android-note
-
/**
* A {@code TimeUnit} represents time durations at a given unit of
* granularity and provides utility methods to convert across units,
@@ -395,4 +391,53 @@ public enum TimeUnit {
Thread.sleep(ms, ns);
}
}
+
+ // BEGIN Android-removed: OpenJDK 9 ChronoUnit related code.
+ /*
+ /**
+ * Converts this {@code TimeUnit} to the equivalent {@code ChronoUnit}.
+ *
+ * @return the converted equivalent ChronoUnit
+ * @since 9
+ *
+ public ChronoUnit toChronoUnit() {
+ switch (this) {
+ case NANOSECONDS: return ChronoUnit.NANOS;
+ case MICROSECONDS: return ChronoUnit.MICROS;
+ case MILLISECONDS: return ChronoUnit.MILLIS;
+ case SECONDS: return ChronoUnit.SECONDS;
+ case MINUTES: return ChronoUnit.MINUTES;
+ case HOURS: return ChronoUnit.HOURS;
+ case DAYS: return ChronoUnit.DAYS;
+ default: throw new AssertionError();
+ }
+ }
+
+ /**
+ * Converts a {@code ChronoUnit} to the equivalent {@code TimeUnit}.
+ *
+ * @param chronoUnit the ChronoUnit to convert
+ * @return the converted equivalent TimeUnit
+ * @throws IllegalArgumentException if {@code chronoUnit} has no
+ * equivalent TimeUnit
+ * @throws NullPointerException if {@code chronoUnit} is null
+ * @since 9
+ *
+ public static TimeUnit of(ChronoUnit chronoUnit) {
+ switch (Objects.requireNonNull(chronoUnit, "chronoUnit")) {
+ case NANOS: return TimeUnit.NANOSECONDS;
+ case MICROS: return TimeUnit.MICROSECONDS;
+ case MILLIS: return TimeUnit.MILLISECONDS;
+ case SECONDS: return TimeUnit.SECONDS;
+ case MINUTES: return TimeUnit.MINUTES;
+ case HOURS: return TimeUnit.HOURS;
+ case DAYS: return TimeUnit.DAYS;
+ default:
+ throw new IllegalArgumentException(
+ "No TimeUnit equivalent for " + chronoUnit);
+ }
+ }
+ */
+ // END Android-removed: OpenJDK 9 ChronoUnit related code.
+
}
diff --git a/org/chromium/support_lib_boundary/FilterMethodsBoundaryInterface.java b/org/chromium/support_lib_boundary/FilterMethodsBoundaryInterface.java
new file mode 100644
index 00000000..b82a934f
--- /dev/null
+++ b/org/chromium/support_lib_boundary/FilterMethodsBoundaryInterface.java
@@ -0,0 +1,5 @@
+package org.chromium.support_lib_boundary;
+
+public interface FilterMethodsBoundaryInterface {
+ void method2(int x);
+}
diff --git a/org/chromium/support_lib_boundary/SingleClassAndMethodBoundaryInterface.java b/org/chromium/support_lib_boundary/SingleClassAndMethodBoundaryInterface.java
new file mode 100644
index 00000000..6bf2b015
--- /dev/null
+++ b/org/chromium/support_lib_boundary/SingleClassAndMethodBoundaryInterface.java
@@ -0,0 +1,5 @@
+package org.chromium.support_lib_boundary;
+
+public interface SingleClassAndMethodBoundaryInterface {
+ void method(boolean param);
+}
diff --git a/org/chromium/support_lib_boundary/WebKitTypeAsMethodParameterBoundaryInterface.java b/org/chromium/support_lib_boundary/WebKitTypeAsMethodParameterBoundaryInterface.java
new file mode 100644
index 00000000..8a7ee539
--- /dev/null
+++ b/org/chromium/support_lib_boundary/WebKitTypeAsMethodParameterBoundaryInterface.java
@@ -0,0 +1,11 @@
+package org.chromium.support_lib_boundary;
+
+import java.lang.reflect.InvocationHandler;
+
+public interface WebKitTypeAsMethodParameterBoundaryInterface {
+ void method(InvocationHandler webViewClient);
+
+ void methodX(InvocationHandler response);
+
+ void urlCall(String url);
+}
diff --git a/org/chromium/support_lib_boundary/WebKitTypeAsMethodReturnBoundaryInterface.java b/org/chromium/support_lib_boundary/WebKitTypeAsMethodReturnBoundaryInterface.java
new file mode 100644
index 00000000..5a83fb6a
--- /dev/null
+++ b/org/chromium/support_lib_boundary/WebKitTypeAsMethodReturnBoundaryInterface.java
@@ -0,0 +1,9 @@
+package org.chromium.support_lib_boundary;
+
+import java.lang.reflect.InvocationHandler;
+
+public interface WebKitTypeAsMethodReturnBoundaryInterface {
+ InvocationHandler method();
+
+ InvocationHandler method2();
+}